Merge changes Ie2944544,Ia7cec977,Id4e5ff33,Iad082ca5 into main

* changes:
  Add support for interruptions in shared values
  Move the implementation of AnimatedState into AnimatedStateImpl
  Make animated values support multiple transitions
  Expose the current scene key in SceneScope
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
index 459c286..515ddc8 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
@@ -19,16 +19,13 @@
 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;
 
@@ -83,7 +80,7 @@
 
     private void prepareForNextRun() {
         SystemClock.sleep(COOL_OFF_PERIOD_MS);
-        waitCoolDownPeriod();
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
         mStartTimeNs = System.nanoTime();
         mPausedDurationNs = 0;
     }
@@ -105,7 +102,7 @@
      * to avoid unnecessary waiting.
      */
     public void resumeTiming() {
-        waitCoolDownPeriod();
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
         resumeTimer();
     }
 
@@ -165,56 +162,4 @@
         }
         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 98ab0c2..762e2af 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -188,6 +188,21 @@
         }
     }
 
+    /** 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 {
@@ -224,6 +239,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -238,6 +254,7 @@
             mRunner.pauseTiming();
             final int userId = createUserNoFlags();
 
+            waitForBroadcastIdle();
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -292,6 +309,9 @@
 
             preStartUser(userId, numberOfIterationsToSkip);
 
+            waitForBroadcastIdle();
+            waitCoolDownPeriod();
+
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -333,6 +353,9 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
 
+            waitForBroadcastIdle();
+            waitCoolDownPeriod();
+
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -397,6 +420,7 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
 
+            waitCoolDownPeriod();
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -430,6 +454,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -441,7 +466,6 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int userId = createUserNoFlags();
-
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -455,6 +479,27 @@
         }
     }
 
+    /** 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 {
@@ -462,7 +507,6 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
-
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -492,6 +536,7 @@
 
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
+            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -517,6 +562,7 @@
                 /* useStaticWallpaper */true);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
+            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -560,6 +606,7 @@
         final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ false);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
+            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -567,6 +614,7 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
+            waitForBroadcastIdle();
             switchUserNoCheck(startUser);
             mRunner.resumeTimingForNextIteration();
         }
@@ -583,6 +631,7 @@
                 /* useStaticWallpaper */ true);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
+            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -590,6 +639,7 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
+            waitForBroadcastIdle();
             switchUserNoCheck(startUser);
             mRunner.resumeTimingForNextIteration();
         }
@@ -625,11 +675,13 @@
     @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();
 
@@ -651,7 +703,7 @@
             final int startUser = mAm.getCurrentUser();
             final int userId = createUserNoFlags();
 
-            mRunner.waitCoolDownPeriod();
+            waitForBroadcastIdle();
             mUserSwitchWaiter.runThenWaitUntilBootCompleted(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -674,7 +726,7 @@
             final int startUser = ActivityManager.getCurrentUser();
             final int userId = createUserNoFlags();
 
-            mRunner.waitCoolDownPeriod();
+            waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilBootCompleted(userId, () -> {
                 mRunner.resumeTiming();
                 Log.d(TAG, "Starting timer");
@@ -700,7 +752,7 @@
                 switchUser(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
 
-            mRunner.waitCoolDownPeriod();
+            waitForBroadcastIdle();
             mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(startUser, () -> {
                 runThenWaitForBroadcasts(userId, () -> {
                     mRunner.resumeTiming();
@@ -729,7 +781,7 @@
                 switchUser(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
 
-            mRunner.waitCoolDownPeriod();
+            waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(startUser, () -> {
                 runThenWaitForBroadcasts(userId, () -> {
                     mRunner.resumeTiming();
@@ -775,6 +827,7 @@
             Log.d(TAG, "Stopping timer");
             attestTrue("Failed creating profile " + userId, mUm.isManagedProfile(userId));
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -815,6 +868,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -859,6 +913,7 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
         removeUser(userId);
@@ -910,6 +965,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -974,6 +1030,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1014,6 +1071,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1066,6 +1124,7 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
+            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1105,6 +1164,7 @@
             runThenWaitForBroadcasts(userId, () -> {
                 startUserInBackgroundAndWaitForUnlock(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
+            waitCoolDownPeriod();
             mRunner.resumeTiming();
             Log.d(TAG, "Starting timer");
 
@@ -1220,7 +1280,6 @@
      * 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);
@@ -1237,7 +1296,7 @@
      */
     private void stopUserAfterWaitingForBroadcastIdle(int userId)
             throws RemoteException {
-        mRunner.waitCoolDownPeriod();
+        waitForBroadcastIdle();
         stopUser(userId);
     }
 
@@ -1379,8 +1438,6 @@
      */
     private void runThenWaitForBroadcasts(int userId, FunctionalUtils.ThrowingRunnable runnable,
             String... actions) {
-        mRunner.waitCoolDownPeriod();
-
         final String unreceivedAction =
                 mBroadcastWaiter.runThenWaitForBroadcasts(userId, runnable, actions);
 
@@ -1481,4 +1538,28 @@
         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/api/system-current.txt b/core/api/system-current.txt
index 96315eb..50d97cf 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -14435,6 +14435,7 @@
     method @NonNull public android.telephony.CarrierRestrictionRules build();
     method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllCarriersAllowed();
     method @NonNull public android.telephony.CarrierRestrictionRules.Builder setAllowedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>);
+    method @FlaggedApi("com.android.internal.telephony.flags.set_carrier_restriction_status") @NonNull public android.telephony.CarrierRestrictionRules.Builder setCarrierRestrictionStatus(int);
     method @NonNull public android.telephony.CarrierRestrictionRules.Builder setDefaultCarrierRestriction(int);
     method @NonNull public android.telephony.CarrierRestrictionRules.Builder setExcludedCarriers(@NonNull java.util.List<android.service.carrier.CarrierIdentifier>);
     method @NonNull public android.telephony.CarrierRestrictionRules.Builder setMultiSimPolicy(int);
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index fd9600c..65628d3 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -16,6 +16,7 @@
 
 package android.accessibilityservice;
 
+import static android.accessibilityservice.AccessibilityServiceInfo.CAPABILITY_CAN_CONTROL_MAGNIFICATION;
 import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
 
@@ -69,6 +70,8 @@
 import android.view.accessibility.AccessibilityWindowInfo;
 import android.view.inputmethod.EditorInfo;
 
+import androidx.annotation.GuardedBy;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.inputmethod.CancellationGroup;
 import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
@@ -640,6 +643,8 @@
         /** The detected gesture information for different displays */
         boolean onGesture(AccessibilityGestureEvent gestureInfo);
         boolean onKeyEvent(KeyEvent event);
+        /** Magnification SystemUI connection changed callbacks */
+        void onMagnificationSystemUIConnectionChanged(boolean connected);
         /** Magnification changed callbacks for different displays */
         void onMagnificationChanged(int displayId, @NonNull Region region,
                 MagnificationConfig config);
@@ -790,7 +795,6 @@
     public static final String KEY_ACCESSIBILITY_SCREENSHOT_TIMESTAMP =
             "screenshot_timestamp";
 
-
     /**
      * Annotations for result codes of attaching accessibility overlays.
      *
@@ -837,6 +841,13 @@
 
     private WindowManager mWindowManager;
 
+    @GuardedBy("mLock")
+    private boolean mServiceConnected;
+    @GuardedBy("mLock")
+    private boolean mMagnificationSystemUIConnected;
+    @GuardedBy("mLock")
+    private boolean mServiceConnectedNotified;
+
     /** List of magnification controllers, mapping from displayId -> MagnificationController. */
     private final SparseArray<MagnificationController> mMagnificationControllers =
             new SparseArray<>(0);
@@ -886,11 +897,14 @@
             for (int i = 0; i < mMagnificationControllers.size(); i++) {
                 mMagnificationControllers.valueAt(i).onServiceConnectedLocked();
             }
+            checkIsMagnificationSystemUIConnectedAlready();
             final AccessibilityServiceInfo info = getServiceInfo();
             if (info != null) {
                 updateInputMethod(info);
                 mMotionEventSources = info.getMotionEventSources();
             }
+            mServiceConnected = true;
+            mServiceConnectedNotified = false;
         }
         if (mSoftKeyboardController != null) {
             mSoftKeyboardController.onServiceConnected();
@@ -898,7 +912,57 @@
 
         // The client gets to handle service connection last, after we've set
         // up any state upon which their code may rely.
-        onServiceConnected();
+        if (android.view.accessibility.Flags
+                .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+            notifyOnServiceConnectedIfReady();
+        } else {
+            onServiceConnected();
+        }
+    }
+
+    private void notifyOnServiceConnectedIfReady() {
+        synchronized (mLock) {
+            if (mServiceConnectedNotified) {
+                return;
+            }
+            boolean canControlMagnification;
+            final AccessibilityServiceInfo info = getServiceInfo();
+            if (info != null) {
+                int flagMask = CAPABILITY_CAN_CONTROL_MAGNIFICATION;
+                canControlMagnification = (info.getCapabilities() & flagMask) == flagMask;
+            } else {
+                canControlMagnification = false;
+            }
+            boolean ready = canControlMagnification
+                    ? (mServiceConnected && mMagnificationSystemUIConnected)
+                    : mServiceConnected;
+            if (ready) {
+                getMainExecutor().execute(() -> onServiceConnected());
+                mServiceConnectedNotified = true;
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void checkIsMagnificationSystemUIConnectedAlready() {
+        if (!android.view.accessibility.Flags
+                .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+            return;
+        }
+        if (mMagnificationSystemUIConnected) {
+            return;
+        }
+        final IAccessibilityServiceConnection connection =
+                AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId);
+        if (connection != null) {
+            try {
+                boolean connected = connection.isMagnificationSystemUIConnected();
+                mMagnificationSystemUIConnected = connected;
+            } catch (RemoteException re) {
+                Log.w(LOG_TAG, "Failed to check magnification system ui connection", re);
+                re.rethrowFromSystemServer();
+            }
+        }
     }
 
     private void updateInputMethod(AccessibilityServiceInfo info) {
@@ -1360,6 +1424,22 @@
         }
     }
 
+    private void onMagnificationSystemUIConnectionChanged(boolean connected) {
+        if (!android.view.accessibility.Flags
+                .waitMagnificationSystemUiConnectionToNotifyServiceConnected()) {
+            return;
+        }
+
+        synchronized (mLock) {
+            boolean changed = (mMagnificationSystemUIConnected != connected);
+            mMagnificationSystemUIConnected = connected;
+
+            if (changed) {
+                notifyOnServiceConnectedIfReady();
+            }
+        }
+    }
+
     private void onMagnificationChanged(int displayId, @NonNull Region region,
             MagnificationConfig config) {
         MagnificationController controller;
@@ -2823,6 +2903,11 @@
             }
 
             @Override
+            public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+                AccessibilityService.this.onMagnificationSystemUIConnectionChanged(connected);
+            }
+
+            @Override
             public void onMagnificationChanged(int displayId, @NonNull Region region,
                     MagnificationConfig config) {
                 AccessibilityService.this.onMagnificationChanged(displayId, region, config);
@@ -3032,6 +3117,16 @@
             });
         }
 
+        @Override
+        public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+            mExecutor.execute(() -> {
+                if (mConnectionId != AccessibilityInteractionClient.NO_ID) {
+                    mCallback.onMagnificationSystemUIConnectionChanged(connected);
+                }
+                return;
+            });
+        }
+
         /** Magnification changed callbacks for different displays */
         public void onMagnificationChanged(int displayId, @NonNull Region region,
                 MagnificationConfig config) {
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
index 3bc61e5..f1479ef 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceClient.aidl
@@ -48,6 +48,8 @@
 
     void onKeyEvent(in KeyEvent event, int sequence);
 
+    void onMagnificationSystemUIConnectionChanged(boolean connected);
+
     void onMagnificationChanged(int displayId, in Region region, in MagnificationConfig config);
 
     void onMotionEvent(in MotionEvent event);
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 713d8e5..149e719 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -130,6 +130,9 @@
     void setMagnificationCallbackEnabled(int displayId, boolean enabled);
 
     @RequiresNoPermission
+    boolean isMagnificationSystemUIConnected();
+
+    @RequiresNoPermission
     boolean setSoftKeyboardShowMode(int showMode);
 
     @RequiresNoPermission
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 2887d22..fa8fe3b 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1755,6 +1755,12 @@
         private int mNavigationBarColor;
         @Appearance
         private int mSystemBarsAppearance;
+        /**
+         * Similar to {@link TaskDescription#mSystemBarsAppearance}, but is taken from the topmost
+         * fully opaque (i.e. non transparent) activity in the task.
+         */
+        @Appearance
+        private int mTopOpaqueSystemBarsAppearance;
         private boolean mEnsureStatusBarContrastWhenTransparent;
         private boolean mEnsureNavigationBarContrastWhenTransparent;
         private int mResizeMode;
@@ -1855,7 +1861,7 @@
                 final Icon icon = mIconRes == Resources.ID_NULL ? null :
                         Icon.createWithResource(ActivityThread.currentPackageName(), mIconRes);
                 return new TaskDescription(mLabel, icon, mPrimaryColor, mBackgroundColor,
-                        mStatusBarColor, mNavigationBarColor, 0, false, false,
+                        mStatusBarColor, mNavigationBarColor, 0, 0, false, false,
                         RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             }
         }
@@ -1874,7 +1880,7 @@
         @Deprecated
         public TaskDescription(String label, @DrawableRes int iconRes, int colorPrimary) {
             this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes),
-                    colorPrimary, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    colorPrimary, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) {
                 throw new RuntimeException("A TaskDescription's primary color should be opaque");
             }
@@ -1892,7 +1898,7 @@
         @Deprecated
         public TaskDescription(String label, @DrawableRes int iconRes) {
             this(label, Icon.createWithResource(ActivityThread.currentPackageName(), iconRes),
-                    0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1904,7 +1910,7 @@
          */
         @Deprecated
         public TaskDescription(String label) {
-            this(label, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+            this(label, null, 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1914,7 +1920,7 @@
          */
         @Deprecated
         public TaskDescription() {
-            this(null, null, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+            this(null, null, 0, 0, 0, 0, 0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
         /**
@@ -1930,7 +1936,7 @@
         @Deprecated
         public TaskDescription(String label, Bitmap icon, int colorPrimary) {
             this(label, icon != null ? Icon.createWithBitmap(icon) : null, colorPrimary, 0, 0, 0,
-                    0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
+                    0, 0, false, false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
             if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) {
                 throw new RuntimeException("A TaskDescription's primary color should be opaque");
             }
@@ -1946,7 +1952,7 @@
          */
         @Deprecated
         public TaskDescription(String label, Bitmap icon) {
-            this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, false,
+            this(label, icon != null ? Icon.createWithBitmap(icon) : null, 0, 0, 0, 0, 0, 0, false,
                     false, RESIZE_MODE_RESIZEABLE, -1, -1, 0);
         }
 
@@ -1955,6 +1961,7 @@
                 int colorPrimary, int colorBackground,
                 int statusBarColor, int navigationBarColor,
                 @Appearance int systemBarsAppearance,
+                @Appearance int topOpaqueSystemBarsAppearance,
                 boolean ensureStatusBarContrastWhenTransparent,
                 boolean ensureNavigationBarContrastWhenTransparent, int resizeMode, int minWidth,
                 int minHeight, int colorBackgroundFloating) {
@@ -1965,6 +1972,7 @@
             mStatusBarColor = statusBarColor;
             mNavigationBarColor = navigationBarColor;
             mSystemBarsAppearance = systemBarsAppearance;
+            mTopOpaqueSystemBarsAppearance = topOpaqueSystemBarsAppearance;
             mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
                     ensureNavigationBarContrastWhenTransparent;
@@ -1994,6 +2002,7 @@
             mStatusBarColor = other.mStatusBarColor;
             mNavigationBarColor = other.mNavigationBarColor;
             mSystemBarsAppearance = other.mSystemBarsAppearance;
+            mTopOpaqueSystemBarsAppearance = other.mTopOpaqueSystemBarsAppearance;
             mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
                     other.mEnsureNavigationBarContrastWhenTransparent;
@@ -2026,6 +2035,9 @@
             if (other.mSystemBarsAppearance != 0) {
                 mSystemBarsAppearance = other.mSystemBarsAppearance;
             }
+            if (other.mTopOpaqueSystemBarsAppearance != 0) {
+                mTopOpaqueSystemBarsAppearance = other.mTopOpaqueSystemBarsAppearance;
+            }
 
             mEnsureStatusBarContrastWhenTransparent = other.mEnsureStatusBarContrastWhenTransparent;
             mEnsureNavigationBarContrastWhenTransparent =
@@ -2305,6 +2317,14 @@
         /**
          * @hide
          */
+        @Appearance
+        public int getTopOpaqueSystemBarsAppearance() {
+            return mTopOpaqueSystemBarsAppearance;
+        }
+
+        /**
+         * @hide
+         */
         public void setEnsureStatusBarContrastWhenTransparent(
                 boolean ensureStatusBarContrastWhenTransparent) {
             mEnsureStatusBarContrastWhenTransparent = ensureStatusBarContrastWhenTransparent;
@@ -2320,6 +2340,13 @@
         /**
          * @hide
          */
+        public void setTopOpaqueSystemBarsAppearance(int topOpaqueSystemBarsAppearance) {
+            mTopOpaqueSystemBarsAppearance = topOpaqueSystemBarsAppearance;
+        }
+
+        /**
+         * @hide
+         */
         public boolean getEnsureNavigationBarContrastWhenTransparent() {
             return mEnsureNavigationBarContrastWhenTransparent;
         }
@@ -2442,6 +2469,7 @@
             dest.writeInt(mStatusBarColor);
             dest.writeInt(mNavigationBarColor);
             dest.writeInt(mSystemBarsAppearance);
+            dest.writeInt(mTopOpaqueSystemBarsAppearance);
             dest.writeBoolean(mEnsureStatusBarContrastWhenTransparent);
             dest.writeBoolean(mEnsureNavigationBarContrastWhenTransparent);
             dest.writeInt(mResizeMode);
@@ -2466,6 +2494,7 @@
             mStatusBarColor = source.readInt();
             mNavigationBarColor = source.readInt();
             mSystemBarsAppearance = source.readInt();
+            mTopOpaqueSystemBarsAppearance = source.readInt();
             mEnsureStatusBarContrastWhenTransparent = source.readBoolean();
             mEnsureNavigationBarContrastWhenTransparent = source.readBoolean();
             mResizeMode = source.readInt();
@@ -2498,7 +2527,8 @@
                     + " resizeMode: " + ActivityInfo.resizeModeToString(mResizeMode)
                     + " minWidth: " + mMinWidth + " minHeight: " + mMinHeight
                     + " colorBackgrounFloating: " + mColorBackgroundFloating
-                    + " systemBarsAppearance: " + mSystemBarsAppearance;
+                    + " systemBarsAppearance: " + mSystemBarsAppearance
+                    + " topOpaqueSystemBarsAppearance: " + mTopOpaqueSystemBarsAppearance;
         }
 
         @Override
@@ -2519,6 +2549,7 @@
             result = result * 31 + mStatusBarColor;
             result = result * 31 + mNavigationBarColor;
             result = result * 31 + mSystemBarsAppearance;
+            result = result * 31 + mTopOpaqueSystemBarsAppearance;
             result = result * 31 + (mEnsureStatusBarContrastWhenTransparent ? 1 : 0);
             result = result * 31 + (mEnsureNavigationBarContrastWhenTransparent ? 1 : 0);
             result = result * 31 + mResizeMode;
@@ -2542,6 +2573,7 @@
                     && mStatusBarColor == other.mStatusBarColor
                     && mNavigationBarColor == other.mNavigationBarColor
                     && mSystemBarsAppearance == other.mSystemBarsAppearance
+                    && mTopOpaqueSystemBarsAppearance == other.mTopOpaqueSystemBarsAppearance
                     && mEnsureStatusBarContrastWhenTransparent
                             == other.mEnsureStatusBarContrastWhenTransparent
                     && mEnsureNavigationBarContrastWhenTransparent
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 7dbf270..76c1ed6 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_MS = 10_100L;
+    private static final long BINDER_CALLBACK_THROTTLE = 10_100L;
     private long mBinderCallbackLast = -1;
 
     /**
@@ -7551,13 +7551,12 @@
             @Override
             public void onTransactionError(int pid, int code, int flags, int err) {
                 final long now = SystemClock.uptimeMillis();
-                if (now < mBinderCallbackLast + BINDER_CALLBACK_THROTTLE_MS) {
+                if (now < mBinderCallbackLast + BINDER_CALLBACK_THROTTLE) {
                     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/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index c0f7232..5956e2b 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -2617,6 +2617,9 @@
         try {
             Objects.requireNonNull(packageName);
             return mPM.isAppArchivable(packageName, new UserHandle(getUserId()));
+        } catch (ParcelableException e) {
+            e.maybeRethrow(NameNotFoundException.class);
+            throw new RuntimeException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 4c839f1..fc3bb02 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -114,7 +114,7 @@
 import com.android.internal.graphics.ColorUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.ContrastColorUtil;
-import com.android.internal.util.NewlineNormalizer;
+import com.android.internal.util.NotificationBigTextNormalizer;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -1632,6 +1632,10 @@
     private Icon mSmallIcon;
     @UnsupportedAppUsage
     private Icon mLargeIcon;
+    private Icon mAppIcon;
+
+    /** Cache for whether the notification was posted by a headless system app. */
+    private Boolean mBelongsToHeadlessSystemApp = null;
 
     @UnsupportedAppUsage
     private String mChannelId;
@@ -3079,25 +3083,17 @@
                     return name.toString();
                 }
             }
-            // If not, try getting the app info from extras.
+            // If not, try getting the name from the app info.
             if (context == null) {
                 return null;
             }
-            final PackageManager pm = context.getPackageManager();
             if (TextUtils.isEmpty(name)) {
-                if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) {
-                    final ApplicationInfo info = extras.getParcelable(
-                            EXTRA_BUILDER_APPLICATION_INFO,
-                            ApplicationInfo.class);
-                    if (info != null) {
-                        name = pm.getApplicationLabel(info);
-                    }
+                ApplicationInfo info = getApplicationInfo(context);
+                if (info != null) {
+                    final PackageManager pm = context.getPackageManager();
+                    name = pm.getApplicationLabel(getApplicationInfo(context));
                 }
             }
-            // If that's still empty, use the one from the context directly.
-            if (TextUtils.isEmpty(name)) {
-                name = pm.getApplicationLabel(context.getApplicationInfo());
-            }
             // If there's still nothing, ¯\_(ツ)_/¯
             if (TextUtils.isEmpty(name)) {
                 return null;
@@ -3109,9 +3105,89 @@
     }
 
     /**
+     * Whether this notification was posted by a headless system app.
+     *
+     * If we don't have enough information to figure this out, this will return false. Therefore,
+     * false negatives are possible, but false positives should not be.
+     *
      * @hide
      */
-    public int loadHeaderAppIconRes(Context context) {
+    public boolean belongsToHeadlessSystemApp(Context context) {
+        Trace.beginSection("Notification#belongsToHeadlessSystemApp");
+
+        try {
+            if (mBelongsToHeadlessSystemApp != null) {
+                return mBelongsToHeadlessSystemApp;
+            }
+
+            if (context == null) {
+                // Without a valid context, we don't know exactly. Let's assume it doesn't belong to
+                // a system app, but not cache the value.
+                return false;
+            }
+
+            ApplicationInfo info = getApplicationInfo(context);
+            if (info != null) {
+                if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+                    // It's not a system app at all.
+                    mBelongsToHeadlessSystemApp = false;
+                } else {
+                    // If there's no launch intent, it's probably a headless app.
+                    final PackageManager pm = context.getPackageManager();
+                    mBelongsToHeadlessSystemApp = pm.getLaunchIntentForPackage(info.packageName)
+                            == null;
+                }
+            } else {
+                // If for some reason we don't have the app info, we don't know; best assume it's
+                // not a system app.
+                return false;
+            }
+            return mBelongsToHeadlessSystemApp;
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    /**
+     * Get the resource ID of the app icon from application info.
+     * @hide
+     */
+    public int getHeaderAppIconRes(Context context) {
+        ApplicationInfo info = getApplicationInfo(context);
+        if (info != null) {
+            return info.icon;
+        }
+        return 0;
+    }
+
+    /**
+     * Load the app icon drawable from the package manager. This could result in a binder call.
+     * @hide
+     */
+    public Drawable loadHeaderAppIcon(Context context) {
+        Trace.beginSection("Notification#loadHeaderAppIcon");
+
+        try {
+            if (context == null) {
+                Log.e(TAG, "Cannot load the app icon drawable with a null context");
+                return null;
+            }
+            final PackageManager pm = context.getPackageManager();
+            ApplicationInfo info = getApplicationInfo(context);
+            if (info == null) {
+                Log.e(TAG, "Cannot load the app icon drawable: no application info");
+                return null;
+            }
+            return pm.getApplicationIcon(info);
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    /**
+     * Fetch the application info from the notification, or the context if that isn't available.
+     */
+    private ApplicationInfo getApplicationInfo(Context context) {
         ApplicationInfo info = null;
         if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) {
             info = extras.getParcelable(
@@ -3119,12 +3195,12 @@
                     ApplicationInfo.class);
         }
         if (info == null) {
+            if (context == null) {
+                return null;
+            }
             info = context.getApplicationInfo();
         }
-        if (info != null) {
-            return info.icon;
-        }
-        return 0;
+        return info;
     }
 
     /**
@@ -3186,12 +3262,12 @@
         return cs.toString();
     }
 
-    private static CharSequence cleanUpNewLines(@Nullable CharSequence charSequence) {
+    private static CharSequence normalizeBigText(@Nullable CharSequence charSequence) {
         if (charSequence == null) {
             return charSequence;
         }
 
-        return NewlineNormalizer.normalizeNewlines(charSequence.toString());
+        return NotificationBigTextNormalizer.normalizeBigText(charSequence.toString());
     }
 
     private static CharSequence removeTextSizeSpans(CharSequence charSequence) {
@@ -4124,6 +4200,55 @@
     }
 
     /**
+     * The colored app icon that can replace the small icon in the notification starting in V.
+     *
+     * Before using this value, you should first check whether it's actually being used by the
+     * notification by calling {@link Notification#shouldUseAppIcon()}.
+     *
+     * @hide
+     */
+    public Icon getAppIcon() {
+        if (mAppIcon != null) {
+            return mAppIcon;
+        }
+        // If the app icon hasn't been loaded yet, check if we can load it without a context.
+        if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) {
+            final ApplicationInfo info = extras.getParcelable(
+                    EXTRA_BUILDER_APPLICATION_INFO,
+                    ApplicationInfo.class);
+            if (info != null) {
+                int appIconRes = info.icon;
+                if (appIconRes == 0) {
+                    Log.w(TAG, "Failed to get the app icon: no icon in application info");
+                    return null;
+                }
+                mAppIcon = Icon.createWithResource(info.packageName, appIconRes);
+                return mAppIcon;
+            } else {
+                Log.e(TAG, "Failed to get the app icon: "
+                        + "there's an EXTRA_BUILDER_APPLICATION_INFO in extras but it's null");
+            }
+        } else {
+            Log.w(TAG, "Failed to get the app icon: no application info in extras");
+        }
+        return null;
+    }
+
+    /**
+     * Whether the notification is using the app icon instead of the small icon.
+     * @hide
+     */
+    public boolean shouldUseAppIcon() {
+        if (Flags.notificationsUseAppIconInRow()) {
+            if (belongsToHeadlessSystemApp(/* context = */ null)) {
+                return false;
+            }
+            return getAppIcon() != null;
+        }
+        return false;
+    }
+
+    /**
      * The large icon shown in this notification's content view.
      * @see Builder#getLargeIcon()
      * @see Builder#setLargeIcon(Icon)
@@ -6116,16 +6241,30 @@
             if (Flags.notificationsUseAppIcon()) {
                 // Override small icon with app icon
                 mN.mSmallIcon = Icon.createWithResource(mContext,
-                        mN.loadHeaderAppIconRes(mContext));
+                        mN.getHeaderAppIconRes(mContext));
             } else if (mN.mSmallIcon == null && mN.icon != 0) {
                 mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon);
             }
 
-            contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon);
+            boolean usingAppIcon = false;
+            if (Flags.notificationsUseAppIconInRow() && !mN.belongsToHeadlessSystemApp(mContext)) {
+                // Use the app icon in the view
+                int appIconRes = mN.getHeaderAppIconRes(mContext);
+                if (appIconRes != 0) {
+                    mN.mAppIcon = Icon.createWithResource(mContext, appIconRes);
+                    contentView.setImageViewIcon(R.id.icon, mN.mAppIcon);
+                    usingAppIcon = true;
+                } else {
+                    Log.w(TAG, "bindSmallIcon: could not get the app icon");
+                }
+            }
+            if (!usingAppIcon) {
+                contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon);
+            }
             contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel);
 
             // Don't change color if we're using the app icon.
-            if (!Flags.notificationsUseAppIcon()) {
+            if (!Flags.notificationsUseAppIcon() && !usingAppIcon) {
                 processSmallIconColor(mN.mSmallIcon, contentView, p);
             }
         }
@@ -8427,7 +8566,7 @@
             // Replace the text with the big text, but only if the big text is not empty.
             CharSequence bigTextText = mBuilder.processLegacyText(mBigText);
             if (Flags.cleanUpSpansAndNewLines()) {
-                bigTextText = cleanUpNewLines(stripStyling(bigTextText));
+                bigTextText = normalizeBigText(stripStyling(bigTextText));
             }
             if (!TextUtils.isEmpty(bigTextText)) {
                 p.text(bigTextText);
diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java
index 348d4d8f..273a79e 100644
--- a/core/java/android/app/UiAutomation.java
+++ b/core/java/android/app/UiAutomation.java
@@ -1969,6 +1969,11 @@
                 }
 
                 @Override
+                public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+                    /* do nothing */
+                }
+
+                @Override
                 public void onMagnificationChanged(int displayId, @NonNull Region region,
                         MagnificationConfig config) {
                     /* do nothing */
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 55c3bb60..2d78317 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -52,14 +52,32 @@
   bug: "281044385"
 }
 
+# vvv Prototypes for using app icons in notifications vvv
+
 flag {
   name: "notifications_use_app_icon"
   namespace: "systemui"
-  description: "Experiment to replace the small icon in a notification with the app icon."
+  description: "Experiment to replace the small icon in a notification with the app icon. This includes the status bar, AOD, shelf and notification row itself."
   bug: "335211019"
 }
 
 flag {
+  name: "notifications_use_app_icon_in_row"
+  namespace: "systemui"
+  description: "Experiment to replace the small icon in a notification row with the app icon."
+  bug: "335211019"
+}
+
+flag {
+  name: "notifications_use_monochrome_app_icon"
+  namespace: "systemui"
+  description: "Experiment to replace the notification icon in the status bar and shelf with the monochrome app icon, if available."
+  bug: "335211019"
+}
+
+# ^^^ Prototypes for using app icons in notifications ^^^
+
+flag {
   name: "notification_expansion_optional"
   namespace: "systemui"
   description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions."
@@ -174,3 +192,10 @@
   description: "Removes all custom views"
   bug: "342602960"
 }
+
+flag {
+  name: "redact_sensitive_content_notifications_on_lockscreen"
+  namespace: "systemui"
+  description: "redacts notifications on the lockscreen if they have the 'sensitiveContent' flag"
+  bug: "343631648"
+}
diff --git a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
index 1c8e497..eb31db1 100644
--- a/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
+++ b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
@@ -27,6 +27,8 @@
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.Objects;
 
 /**
@@ -38,7 +40,9 @@
     private static final String TAG = "WindowStateInsetsControlChangeItem";
 
     private InsetsState mInsetsState;
-    private InsetsSourceControl.Array mActiveControls;
+
+    @VisibleForTesting
+    public InsetsSourceControl.Array mActiveControls;
 
     @Override
     public void execute(@NonNull ClientTransactionHandler client, @NonNull IWindow window,
@@ -51,6 +55,8 @@
             // An exception could happen if the process is restarted. It is safe to ignore since
             // the window should no longer exist.
             Log.w(TAG, "The original window no longer exists in the new process", e);
+            // Prevent leak
+            mActiveControls.release();
         }
         Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
     }
@@ -69,7 +75,12 @@
         }
         instance.setWindow(window);
         instance.mInsetsState = new InsetsState(insetsState, true /* copySources */);
-        instance.mActiveControls = new InsetsSourceControl.Array(activeControls);
+        instance.mActiveControls = new InsetsSourceControl.Array(
+                activeControls, true /* copyControls */);
+        // This source control is an extra copy if the client is not local. By setting
+        // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of
+        // SurfaceControl.writeToParcel.
+        instance.mActiveControls.setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE);
 
         return instance;
     }
diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig
index 374be6f..18cfca6 100644
--- a/core/java/android/appwidget/flags.aconfig
+++ b/core/java/android/appwidget/flags.aconfig
@@ -40,3 +40,13 @@
   description: "Throttle the widget view updates to mitigate transaction exceptions"
   bug: "326145514"
 }
+
+flag {
+  name: "support_resume_restore_after_reboot"
+  namespace: "app_widgets"
+  description: "Enable support for resume restore widget after reboot by persisting intermediate states to disk"
+  bug: "336976070"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index ed5d662..1e781532 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -64,3 +64,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "virtual_devices"
+    name: "intent_interception_action_matching_fix"
+    description: "Do not match intents without actions if the filter has actions"
+    bug: "343805037"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 270fc32..bbd0e9f 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -2431,6 +2431,7 @@
                     statusReceiver, new UserHandle(mUserId));
         } catch (ParcelableException e) {
             e.maybeRethrow(PackageManager.NameNotFoundException.class);
+            throw new RuntimeException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2467,6 +2468,7 @@
         } catch (ParcelableException e) {
             e.maybeRethrow(IOException.class);
             e.maybeRethrow(PackageManager.NameNotFoundException.class);
+            throw new RuntimeException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2499,6 +2501,7 @@
                     userActionIntent, new UserHandle(mUserId));
         } catch (ParcelableException e) {
             e.maybeRethrow(PackageManager.NameNotFoundException.class);
+            throw new RuntimeException(e);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index e2a131c..5668c54 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -140,6 +140,16 @@
   }
 }
 
+flag {
+    name: "fix_avatar_concurrent_file_write"
+    namespace: "multiuser"
+    description: "Fix potential unexpected behavior due to concurrent file writing"
+    bug: "339351031"
+    metadata {
+    	purpose: PURPOSE_BUGFIX
+  }
+}
+
 # This flag guards the private space feature and all its implementations excluding the APIs. APIs are guarded by android.os.Flags.allow_private_profile.
 flag {
     name: "enable_private_space_features"
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index e3780ed..f54be00 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -733,7 +733,7 @@
      * commits, or is rolled back, either explicitly or by a call to
      * {@link #yieldIfContendedSafely}.
      */
-    // TODO(340874899) Provide an Executor overload
+    @SuppressLint("ExecutorRegistration")
     public void beginTransactionWithListener(
             @Nullable SQLiteTransactionListener transactionListener) {
         beginTransaction(transactionListener, true);
@@ -763,7 +763,7 @@
      *            transaction begins, commits, or is rolled back, either
      *            explicitly or by a call to {@link #yieldIfContendedSafely}.
      */
-    // TODO(340874899) Provide an Executor overload
+    @SuppressLint("ExecutorRegistration")
     public void beginTransactionWithListenerNonExclusive(
             @Nullable SQLiteTransactionListener transactionListener) {
         beginTransaction(transactionListener, false);
@@ -789,7 +789,6 @@
      *   }
      * </pre>
      */
-    // TODO(340874899) Provide an Executor overload
     @SuppressLint("ExecutorRegistration")
     @FlaggedApi(Flags.FLAG_SQLITE_APIS_35)
     public void beginTransactionWithListenerReadOnly(
diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig
index 4284ad0..047d1fa 100644
--- a/core/java/android/hardware/biometrics/flags.aconfig
+++ b/core/java/android/hardware/biometrics/flags.aconfig
@@ -32,3 +32,10 @@
   description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder."
   bug: "302735104"
 }
+
+flag {
+  name: "mandatory_biometrics"
+  namespace: "biometrics_framework"
+  description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations"
+  bug: "322081563"
+}
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 342479b..3cc87ea 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -1492,10 +1492,12 @@
     /**
      * <p>Default flash brightness level for manual flash control in SINGLE mode.</p>
      * <p>If flash unit is available this will be greater than or equal to 1 and less
-     * or equal to <code>android.flash.info.singleStrengthMaxLevel</code>.
+     * or equal to {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}.
      * Note for devices that do not support the manual flash strength control
      * feature, this level will always be equal to 1.</p>
      * <p>This key is available on all devices.</p>
+     *
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
      */
     @PublicKey
     @NonNull
@@ -1511,13 +1513,15 @@
      * otherwise the value will be equal to 1.</p>
      * <p>Note that this level is just a number of supported levels(the granularity of control).
      * There is no actual physical power units tied to this level.
-     * There is no relation between android.flash.info.torchStrengthMaxLevel and
-     * android.flash.info.singleStrengthMaxLevel i.e. the ratio of
-     * android.flash.info.torchStrengthMaxLevel:android.flash.info.singleStrengthMaxLevel
+     * There is no relation between {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} and
+     * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} i.e. the ratio of
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}:{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}
      * is not guaranteed to be the ratio of actual brightness.</p>
      * <p>This key is available on all devices.</p>
      *
      * @see CaptureRequest#FLASH_MODE
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
      */
     @PublicKey
     @NonNull
@@ -1528,10 +1532,12 @@
     /**
      * <p>Default flash brightness level for manual flash control in TORCH mode</p>
      * <p>If flash unit is available this will be greater than or equal to 1 and less
-     * or equal to android.flash.info.torchStrengthMaxLevel.
+     * or equal to {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}.
      * Note for the devices that do not support the manual flash strength control feature,
      * this level will always be equal to 1.</p>
      * <p>This key is available on all devices.</p>
+     *
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
      */
     @PublicKey
     @NonNull
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index c82e7e8..938636f 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2684,35 +2684,39 @@
      * <p>Flash strength level to be used when manual flash control is active.</p>
      * <p>Flash strength level to use in capture mode i.e. when the applications control
      * flash with either SINGLE or TORCH mode.</p>
-     * <p>Use android.flash.info.singleStrengthMaxLevel and
-     * android.flash.info.torchStrengthMaxLevel to check whether the device supports
+     * <p>Use {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} to check whether the device supports
      * flash strength control or not.
      * If the values of android.flash.info.singleStrengthMaxLevel and
-     * android.flash.info.torchStrengthMaxLevel are greater than 1,
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1,
      * then the device supports manual flash strength control.</p>
      * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be &gt;= 1
-     * and &lt;= android.flash.info.torchStrengthMaxLevel.
+     * and &lt;= {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}.
      * If the application doesn't set the key and
-     * android.flash.info.torchStrengthMaxLevel &gt; 1,
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} &gt; 1,
      * then the flash will be fired at the default level set by HAL in
-     * android.flash.info.torchStrengthDefaultLevel.
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL android.flash.torchStrengthDefaultLevel}.
      * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be &gt;= 1
-     * and &lt;= android.flash.info.singleStrengthMaxLevel.
+     * and &lt;= {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}.
      * If the application does not set this key and
-     * android.flash.info.singleStrengthMaxLevel &gt; 1,
+     * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} &gt; 1,
      * then the flash will be fired at the default level set by HAL
-     * in android.flash.info.singleStrengthDefaultLevel.
+     * in {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL android.flash.singleStrengthDefaultLevel}.
      * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH,
      * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p>
      * <p><b>Range of valid values:</b><br>
-     * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * <code>[1-{@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
      * set to TORCH;
-     * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * <code>[1-{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
      * set to SINGLE</p>
      * <p>This key is available on all devices.</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
      * @see CaptureRequest#FLASH_MODE
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
      */
     @PublicKey
     @NonNull
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 1460515..4406a41 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -2977,35 +2977,39 @@
      * <p>Flash strength level to be used when manual flash control is active.</p>
      * <p>Flash strength level to use in capture mode i.e. when the applications control
      * flash with either SINGLE or TORCH mode.</p>
-     * <p>Use android.flash.info.singleStrengthMaxLevel and
-     * android.flash.info.torchStrengthMaxLevel to check whether the device supports
+     * <p>Use {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} and
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} to check whether the device supports
      * flash strength control or not.
      * If the values of android.flash.info.singleStrengthMaxLevel and
-     * android.flash.info.torchStrengthMaxLevel are greater than 1,
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} are greater than 1,
      * then the device supports manual flash strength control.</p>
      * <p>If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> TORCH the value must be &gt;= 1
-     * and &lt;= android.flash.info.torchStrengthMaxLevel.
+     * and &lt;= {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}.
      * If the application doesn't set the key and
-     * android.flash.info.torchStrengthMaxLevel &gt; 1,
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel} &gt; 1,
      * then the flash will be fired at the default level set by HAL in
-     * android.flash.info.torchStrengthDefaultLevel.
+     * {@link CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL android.flash.torchStrengthDefaultLevel}.
      * If the {@link CaptureRequest#FLASH_MODE android.flash.mode} <code>==</code> SINGLE, then the value must be &gt;= 1
-     * and &lt;= android.flash.info.singleStrengthMaxLevel.
+     * and &lt;= {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}.
      * If the application does not set this key and
-     * android.flash.info.singleStrengthMaxLevel &gt; 1,
+     * {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel} &gt; 1,
      * then the flash will be fired at the default level set by HAL
-     * in android.flash.info.singleStrengthDefaultLevel.
+     * in {@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL android.flash.singleStrengthDefaultLevel}.
      * If {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is set to any of ON_AUTO_FLASH, ON_ALWAYS_FLASH,
      * ON_AUTO_FLASH_REDEYE, ON_EXTERNAL_FLASH values, then the strengthLevel will be ignored.</p>
      * <p><b>Range of valid values:</b><br>
-     * <code>[1-android.flash.info.torchStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * <code>[1-{@link CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL android.flash.torchStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
      * set to TORCH;
-     * <code>[1-android.flash.info.singleStrengthMaxLevel]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
+     * <code>[1-{@link CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL android.flash.singleStrengthMaxLevel}]</code> when the {@link CaptureRequest#FLASH_MODE android.flash.mode} is
      * set to SINGLE</p>
      * <p>This key is available on all devices.</p>
      *
      * @see CaptureRequest#CONTROL_AE_MODE
      * @see CaptureRequest#FLASH_MODE
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_DEFAULT_LEVEL
+     * @see CameraCharacteristics#FLASH_SINGLE_STRENGTH_MAX_LEVEL
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_DEFAULT_LEVEL
+     * @see CameraCharacteristics#FLASH_TORCH_STRENGTH_MAX_LEVEL
      */
     @PublicKey
     @NonNull
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 9eabc8d..53771e3 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -963,11 +963,10 @@
      * originate from the system, just that we were unable to verify it. This can
      * happen for a number of reasons during normal operation.
      *
-     * @param event The {@link android.view.InputEvent} to check
+     * @param event The {@link android.view.InputEvent} to check.
      *
      * @return {@link android.view.VerifiedInputEvent}, which is a subset of the provided
-     * {@link android.view.InputEvent}
-     *         {@code null} if the event could not be verified.
+     *     {@link android.view.InputEvent}, or {@code null} if the event could not be verified.
      */
     @Nullable
     public VerifiedInputEvent verifyInputEvent(@NonNull InputEvent event) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f357ebf..7933212 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -19930,6 +19930,12 @@
             public static final String NETWORK_LOCATION_OPT_IN = "network_location_opt_in";
 
             /**
+             * Whether haptics are enabled for Active Unlock on wear.
+             * @hide
+             */
+            public static final String VIBRATE_FOR_ACTIVE_UNLOCK = "wear_vibrate_for_active_unlock";
+
+            /**
              * The custom foreground color.
              * @hide
              */
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 71066ac..3f9c819 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1276,13 +1276,22 @@
         });
     }
 
+    /**
+     * Whether or not wake requests will be redirected.
+     *
+     * @hide
+     */
+    public boolean getRedirectWake() {
+        return mOverlayConnection != null && mRedirectWake;
+    }
+
     private void wakeUp(boolean fromSystem) {
         if (mDebug) {
             Slog.v(mTag, "wakeUp(): fromSystem=" + fromSystem + ", mWaking=" + mWaking
                     + ", mFinished=" + mFinished);
         }
 
-        if (!fromSystem && mOverlayConnection != null && mRedirectWake) {
+        if (!fromSystem && getRedirectWake()) {
             mOverlayConnection.addConsumer(overlay -> {
                 try {
                     overlay.onWakeRequested();
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index c674968..0dec13f 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,9 +18,10 @@
 
 import static com.android.graphics.hwui.flags.Flags.highContrastTextLuminance;
 import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
-import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
 import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION;
+import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
 
+import android.annotation.ColorInt;
 import android.annotation.FlaggedApi;
 import android.annotation.FloatRange;
 import android.annotation.IntDef;
@@ -398,6 +399,20 @@
         mUseBoundsForWidth = useBoundsForWidth;
         mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
         mMinimumFontMetrics = minimumFontMetrics;
+
+        initSpanColors();
+    }
+
+    private void initSpanColors() {
+        if (mSpannedText && Flags.highContrastTextSmallTextRect()) {
+            if (mSpanColors == null) {
+                mSpanColors = new SpanColors();
+            } else {
+                mSpanColors.recycle();
+            }
+        } else {
+            mSpanColors = null;
+        }
     }
 
     /**
@@ -417,6 +432,7 @@
         mSpacingMult = spacingmult;
         mSpacingAdd = spacingadd;
         mSpannedText = text instanceof Spanned;
+        initSpanColors();
     }
 
     /**
@@ -643,20 +659,20 @@
             return null;
         }
 
-        return isHighContrastTextDark() ? BlendMode.MULTIPLY : BlendMode.DIFFERENCE;
+        return isHighContrastTextDark(mPaint.getColor()) ? BlendMode.MULTIPLY
+                : BlendMode.DIFFERENCE;
     }
 
-    private boolean isHighContrastTextDark() {
+    private boolean isHighContrastTextDark(@ColorInt int color) {
         // High-contrast text mode
         // Determine if the text is black-on-white or white-on-black, so we know what blendmode will
         // give the highest contrast and most realistic text color.
         // This equation should match the one in libs/hwui/hwui/DrawTextFunctor.h
         if (highContrastTextLuminance()) {
             var lab = new double[3];
-            ColorUtils.colorToLAB(mPaint.getColor(), lab);
-            return lab[0] < 0.5;
+            ColorUtils.colorToLAB(color, lab);
+            return lab[0] < 50.0;
         } else {
-            var color = mPaint.getColor();
             int channelSum = Color.red(color) + Color.green(color) + Color.blue(color);
             return channelSum < (128 * 3);
         }
@@ -1010,15 +1026,22 @@
         var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
                 mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
 
+        var originalTextColor = mPaint.getColor();
         var bgPaint = mWorkPlainPaint;
         bgPaint.reset();
-        bgPaint.setColor(isHighContrastTextDark() ? Color.WHITE : Color.BLACK);
+        bgPaint.setColor(isHighContrastTextDark(originalTextColor) ? Color.WHITE : Color.BLACK);
         bgPaint.setStyle(Paint.Style.FILL);
 
         int start = getLineStart(firstLine);
         int end = getLineEnd(lastLine);
         // Draw a separate background rectangle for each line of text, that only surrounds the
-        // characters on that line.
+        // characters on that line. But we also have to check the text color for each character, and
+        // make sure we are drawing the correct contrasting background. This is because Spans can
+        // change colors throughout the text and we'll need to match our backgrounds.
+        if (mSpannedText && mSpanColors != null) {
+            mSpanColors.init(mWorkPaint, ((Spanned) mText), start, end);
+        }
+
         forEachCharacterBounds(
                 start,
                 end,
@@ -1028,13 +1051,24 @@
                     int mLastLineNum = -1;
                     final RectF mLineBackground = new RectF();
 
+                    @ColorInt int mLastColor = originalTextColor;
+
                     @Override
                     public void onCharacterBounds(int index, int lineNum, float left, float top,
                             float right, float bottom) {
-                        if (lineNum != mLastLineNum) {
+
+                        var newBackground = determineContrastingBackgroundColor(index);
+                        var hasBgColorChanged = newBackground != bgPaint.getColor();
+
+                        if (lineNum != mLastLineNum || hasBgColorChanged) {
+                            // Draw what we have so far, then reset the rect and update its color
                             drawRect();
                             mLineBackground.set(left, top, right, bottom);
                             mLastLineNum = lineNum;
+
+                            if (hasBgColorChanged) {
+                                bgPaint.setColor(newBackground);
+                            }
                         } else {
                             mLineBackground.union(left, top, right, bottom);
                         }
@@ -1051,8 +1085,36 @@
                             canvas.drawRect(mLineBackground, bgPaint);
                         }
                     }
+
+                    private int determineContrastingBackgroundColor(int index) {
+                        if (!mSpannedText || mSpanColors == null) {
+                            // The text is not Spanned. it's all one color.
+                            return bgPaint.getColor();
+                        }
+
+                        // Sometimes the color will change, but not enough to warrant a background
+                        // color change. e.g. from black to dark grey still gets clamped to black,
+                        // so the background stays white and we don't need to draw a fresh
+                        // background.
+                        var textColor = mSpanColors.getColorAt(index);
+                        if (textColor == SpanColors.NO_COLOR_FOUND) {
+                            textColor = originalTextColor;
+                        }
+                        var hasColorChanged = textColor != mLastColor;
+                        if (hasColorChanged) {
+                            mLastColor = textColor;
+
+                            return isHighContrastTextDark(textColor) ? Color.WHITE : Color.BLACK;
+                        }
+
+                        return bgPaint.getColor();
+                    }
                 }
         );
+
+        if (mSpanColors != null) {
+            mSpanColors.recycle();
+        }
     }
 
     /**
@@ -3580,6 +3642,7 @@
     private float mSpacingAdd;
     private static final Rect sTempRect = new Rect();
     private boolean mSpannedText;
+    @Nullable private SpanColors mSpanColors;
     private TextDirectionHeuristic mTextDir;
     private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
     private boolean mIncludePad;
diff --git a/core/java/android/text/SpanColors.java b/core/java/android/text/SpanColors.java
new file mode 100644
index 0000000..fcd242b
--- /dev/null
+++ b/core/java/android/text/SpanColors.java
@@ -0,0 +1,89 @@
+/*
+ * 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.text;
+
+import android.annotation.ColorInt;
+import android.annotation.Nullable;
+import android.graphics.Color;
+import android.text.style.CharacterStyle;
+
+/**
+ * Finds the foreground text color for the given Spanned text so you can iterate through each color
+ * change.
+ *
+ * @hide
+ */
+public class SpanColors {
+    public static final @ColorInt int NO_COLOR_FOUND = Color.TRANSPARENT;
+
+    private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
+            new SpanSet<>(CharacterStyle.class);
+    @Nullable private TextPaint mWorkPaint;
+
+    public SpanColors() {}
+
+    /**
+     * Init for the given text
+     *
+     * @param workPaint A temporary TextPaint object that will be used to calculate the colors. The
+     *                  paint properties will be mutated on calls to {@link #getColorAt(int)} so
+     *                  make sure to reset it before you use it for something else.
+     * @param spanned the text to examine
+     * @param start index to start at
+     * @param end index of the end
+     */
+    public void init(TextPaint workPaint, Spanned spanned, int start, int end) {
+        mWorkPaint = workPaint;
+        mCharacterStyleSpanSet.init(spanned, start, end);
+    }
+
+    /**
+     * Removes all internal references to the spans to avoid memory leaks.
+     */
+    public void recycle() {
+        mWorkPaint = null;
+        mCharacterStyleSpanSet.recycle();
+    }
+
+    /**
+     * Calculates the foreground color of the text at the given character index.
+     *
+     * <p>You must call {@link #init(TextPaint, Spanned, int, int)} before calling this
+     */
+    public @ColorInt int getColorAt(int index) {
+        var finalColor = NO_COLOR_FOUND;
+        // Reset the paint so if we get a CharacterStyle that doesn't actually specify color,
+        // (like UnderlineSpan), we still return no color found.
+        mWorkPaint.setColor(finalColor);
+        for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
+            if ((index >= mCharacterStyleSpanSet.spanStarts[k])
+                    && (index <= mCharacterStyleSpanSet.spanEnds[k])) {
+                final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
+                span.updateDrawState(mWorkPaint);
+
+                finalColor = calculateFinalColor(mWorkPaint);
+            }
+        }
+        return finalColor;
+    }
+
+    private @ColorInt int calculateFinalColor(TextPaint workPaint) {
+        // TODO: can we figure out what the getColorFilter() will do?
+        //  if so, we also need to reset colorFilter before the loop in getColorAt()
+        return workPaint.getColor();
+    }
+}
diff --git a/core/java/android/tracing/perfetto/InitArguments.java b/core/java/android/tracing/perfetto/InitArguments.java
index da8c273..b4cb68c 100644
--- a/core/java/android/tracing/perfetto/InitArguments.java
+++ b/core/java/android/tracing/perfetto/InitArguments.java
@@ -26,6 +26,7 @@
  */
 public class InitArguments {
     public final @PerfettoBackend int backends;
+    public final int shmemSizeHintKb;
 
     /**
      * @hide
@@ -44,11 +45,21 @@
     // on Linux/Android/Mac uses a named UNIX socket).
     public static final int PERFETTO_BACKEND_SYSTEM = (1 << 1);
 
-    public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM);
+    public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM, 0);
 
-    public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS);
+    public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS, 0);
 
-    public InitArguments(@PerfettoBackend int backends) {
+    /**
+     * Perfetto initialization arguments.
+     *
+     * @param backends Bitwise-or of backends that should be enabled.
+     * @param shmemSizeHintKb [Optional] Tune the size of the shared memory buffer between the
+     *  current process and the service backend(s). This is a trade-off between memory footprint and
+     *  the ability to sustain bursts of trace writes. If set, the value must be a multiple of 4KB.
+     *  The value can be ignored if larger than kMaxShmSize (32MB) or not a multiple of 4KB.
+     */
+    public InitArguments(@PerfettoBackend int backends, int shmemSizeHintKb) {
         this.backends = backends;
+        this.shmemSizeHintKb = shmemSizeHintKb;
     }
 }
diff --git a/core/java/android/tracing/perfetto/Producer.java b/core/java/android/tracing/perfetto/Producer.java
index a1b3eb7..13582e8 100644
--- a/core/java/android/tracing/perfetto/Producer.java
+++ b/core/java/android/tracing/perfetto/Producer.java
@@ -27,8 +27,8 @@
      * @param args arguments on how to initialize the Perfetto producer.
      */
     public static void init(InitArguments args) {
-        nativePerfettoProducerInit(args.backends);
+        nativePerfettoProducerInit(args.backends, args.shmemSizeHintKb);
     }
 
-    private static native void nativePerfettoProducerInit(int backends);
+    private static native void nativePerfettoProducerInit(int backends, int shmemSizeHintKb);
 }
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index ba1915c..15b0c13 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2263,6 +2263,7 @@
             this(modeId, width, height, refreshRate, vsyncRate, false, alternativeRefreshRates,
                     supportedHdrTypes);
         }
+
         /**
          * @hide
          */
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index da86e2d..8b9d876 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -213,7 +213,7 @@
 
     /**
      * The supported modes that will be exposed externally.
-     * Might have different set of modes that supportedModes for VRR displays
+     * Might have different set of modes than supportedModes for VRR displays
      */
     public Display.Mode[] appsSupportedModes = Display.Mode.EMPTY_ARRAY;
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index a9846fb..eec805b7 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -303,7 +303,7 @@
     }
 
     /** Not running an animation. */
-    @VisibleForTesting(visibility = PACKAGE)
+    @VisibleForTesting
     public static final int ANIMATION_TYPE_NONE = -1;
 
     /** Running animation will show insets */
@@ -317,7 +317,7 @@
     public static final int ANIMATION_TYPE_USER = 2;
 
     /** Running animation will resize insets */
-    @VisibleForTesting(visibility = PACKAGE)
+    @VisibleForTesting
     public static final int ANIMATION_TYPE_RESIZE = 3;
 
     @Retention(RetentionPolicy.SOURCE)
@@ -1712,7 +1712,7 @@
         mImeSourceConsumer.onWindowFocusLost();
     }
 
-    @VisibleForTesting(visibility = PACKAGE)
+    @VisibleForTesting
     public @AnimationType int getAnimationType(@InsetsType int type) {
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
             InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index 6c670f5..fdb2a6e 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -17,7 +17,6 @@
 package android.view;
 
 import static android.view.InsetsController.ANIMATION_TYPE_NONE;
-import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
 import static android.view.InsetsController.AnimationType;
 import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE;
@@ -32,7 +31,6 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
@@ -181,11 +179,10 @@
                     mController.notifyVisibilityChanged();
                 }
 
-                // If there is no animation controlling the leash, make sure the visibility and the
-                // position is up-to-date.
-                final int animType = mController.getAnimationType(mType);
-                if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) {
-                    applyRequestedVisibilityAndPositionToControl();
+                // If we have a new leash, make sure visibility is up-to-date, even though we
+                // didn't want to run an animation above.
+                if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) {
+                    applyRequestedVisibilityToControl();
                 }
 
                 // Remove the surface that owned by last control when it lost.
@@ -374,27 +371,21 @@
         if (DEBUG) Log.d(TAG, "updateSource: " + newSource);
     }
 
-    private void applyRequestedVisibilityAndPositionToControl() {
-        if (mSourceControl == null) {
-            return;
-        }
-        final SurfaceControl leash = mSourceControl.getLeash();
-        if (leash == null) {
+    private void applyRequestedVisibilityToControl() {
+        if (mSourceControl == null || mSourceControl.getLeash() == null) {
             return;
         }
 
         final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
-        final Point surfacePosition = mSourceControl.getSurfacePosition();
         try (Transaction t = mTransactionSupplier.get()) {
             if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible);
             if (requestedVisible) {
-                t.show(leash);
+                t.show(mSourceControl.getLeash());
             } else {
-                t.hide(leash);
+                t.hide(mSourceControl.getLeash());
             }
             // Ensure the alpha value is aligned with the actual requested visibility.
-            t.setAlpha(leash, requestedVisible ? 1 : 0);
-            t.setPosition(leash, surfacePosition.x, surfacePosition.y);
+            t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0);
             t.apply();
         }
         onPerceptible(requestedVisible);
diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 4e5cb58..487214c 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -269,22 +269,66 @@
         public Array() {
         }
 
-        public Array(@NonNull Array other) {
-            mControls = other.mControls;
+        /**
+         * @param copyControls whether or not to make a copy of the each {@link InsetsSourceControl}
+         */
+        public Array(@NonNull Array other, boolean copyControls) {
+            setTo(other, copyControls);
         }
 
-        public Array(Parcel in) {
+        public Array(@NonNull Parcel in) {
             readFromParcel(in);
         }
 
-        public void set(@Nullable InsetsSourceControl[] controls) {
-            mControls = controls;
+        /** Updates the current Array to the given Array. */
+        public void setTo(@NonNull Array other, boolean copyControls) {
+            set(other.mControls, copyControls);
         }
 
+        /** Updates the current controls to the given controls. */
+        public void set(@Nullable InsetsSourceControl[] controls, boolean copyControls) {
+            if (controls == null || !copyControls) {
+                mControls = controls;
+                return;
+            }
+            // Make a copy of the array.
+            mControls = new InsetsSourceControl[controls.length];
+            for (int i = mControls.length - 1; i >= 0; i--) {
+                if (controls[i] != null) {
+                    mControls[i] = new InsetsSourceControl(controls[i]);
+                }
+            }
+        }
+
+        /** Gets the controls. */
         public @Nullable InsetsSourceControl[] get() {
             return mControls;
         }
 
+        /** Cleanup {@link SurfaceControl} stored in controls to prevent leak. */
+        public void release() {
+            if (mControls == null) {
+                return;
+            }
+            for (InsetsSourceControl control : mControls) {
+                if (control != null) {
+                    control.release(SurfaceControl::release);
+                }
+            }
+        }
+
+        /** Sets the given flags to all controls. */
+        public void setParcelableFlags(int parcelableFlags) {
+            if (mControls == null) {
+                return;
+            }
+            for (InsetsSourceControl control : mControls) {
+                if (control != null) {
+                    control.setParcelableFlags(parcelableFlags);
+                }
+            }
+        }
+
         @Override
         public int describeContents() {
             return 0;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 1cb2765..075ee1b 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -23705,12 +23705,6 @@
                     mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                     mPrivateFlags &= ~PFLAG_DIRTY_MASK;
 
-                    // // For VRR to vote the preferred frame rate
-                    if (sToolkitSetFrameRateReadOnlyFlagValue
-                            && sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
-                        votePreferredFrameRate();
-                    }
-
                     mPrivateFlags4 |= PFLAG4_HAS_DRAWN;
 
                     // Fast path for layouts with no backgrounds
@@ -23727,6 +23721,12 @@
                         draw(canvas);
                     }
                 }
+
+                // For VRR to vote the preferred frame rate
+                if (sToolkitSetFrameRateReadOnlyFlagValue
+                        && sToolkitFrameRateViewEnablingReadOnlyFlagValue) {
+                    votePreferredFrameRate();
+                }
             } finally {
                 renderNode.endRecording();
                 setDisplayListProperties(renderNode);
@@ -34061,10 +34061,14 @@
     }
 
     private float convertVelocityToFrameRate(float velocityPps) {
+        // From UXR study, premium experience is:
+        // 1500+    dp/s: 120fps
+        // 0 - 1500 dp/s:  80fps
+        // OEMs are likely to modify this to balance battery and user experience for their
+        // specific device.
         float density = mAttachInfo.mDensity;
         float velocityDps = velocityPps / density;
-        // Choose a frame rate in increments of 10fps
-        return Math.min(MAX_FRAME_RATE, 60f + (10f * (float) Math.floor(velocityDps / 300f)));
+        return (velocityDps >= 1500f) ? MAX_FRAME_RATE : 80f;
     }
 
     /**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 139285a..fd407d6 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2276,6 +2276,29 @@
         requestLayout();
     }
 
+    /** Handles messages {@link #MSG_INSETS_CONTROL_CHANGED}. */
+    private void handleInsetsControlChanged(@NonNull InsetsState insetsState,
+            @NonNull InsetsSourceControl.Array activeControls) {
+        final InsetsSourceControl[] controls = activeControls.get();
+
+        if (mTranslator != null) {
+            mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
+            mTranslator.translateSourceControlsInScreenToAppWindow(controls);
+        }
+
+        // Deliver state change before control change, such that:
+        // a) When gaining control, controller can compare with server state to evaluate
+        // whether it needs to run animation.
+        // b) When loosing control, controller can restore server state by taking last
+        // dispatched state as truth.
+        mInsetsController.onStateChanged(insetsState);
+        if (mAdded) {
+            mInsetsController.onControlsChanged(controls);
+        } else {
+            activeControls.release();
+        }
+    }
+
     private final DisplayListener mDisplayListener = new DisplayListener() {
         @Override
         public void onDisplayChanged(int displayId) {
@@ -2767,11 +2790,27 @@
     public void bringChildToFront(View child) {
     }
 
+    // keep in sync with getHostVisibilityReason
     int getHostVisibility() {
         return mView != null && (mAppVisible || mForceDecorViewVisibility)
                 ? mView.getVisibility() : View.GONE;
     }
 
+    String getHostVisibilityReason() {
+        if (mView == null) {
+            return "mView is null";
+        }
+        if (!mAppVisible && !mForceDecorViewVisibility) {
+            return "!mAppVisible && !mForceDecorViewVisibility";
+        }
+        switch (mView.getVisibility()) {
+            case View.VISIBLE: return "View.VISIBLE";
+            case View.GONE: return "View.GONE";
+            case View.INVISIBLE: return "View.INVISIBLE";
+            default: return "";
+        }
+    }
+
     /**
      * Add LayoutTransition to the list of transitions to be started in the next traversal.
      * This list will be cleared after the transitions on the list are start()'ed. These
@@ -3323,6 +3362,7 @@
         int desiredWindowHeight;
 
         final int viewVisibility = getHostVisibility();
+        final String viewVisibilityReason = getHostVisibilityReason();
         final boolean viewVisibilityChanged = !mFirst
                 && (mViewVisibility != viewVisibility || mNewSurfaceNeeded
                 // Also check for possible double visibility update, which will make current
@@ -4197,7 +4237,7 @@
 
         if (!isViewVisible) {
             if (mLastTraversalWasVisible) {
-                logAndTrace("Not drawing due to not visible");
+                logAndTrace("Not drawing due to not visible. Reason=" + viewVisibilityReason);
             }
             mLastPerformTraversalsSkipDrawReason = "view_not_visible";
             if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
@@ -6591,24 +6631,11 @@
                     break;
                 }
                 case MSG_INSETS_CONTROL_CHANGED: {
-                    SomeArgs args = (SomeArgs) msg.obj;
-
-                    // Deliver state change before control change, such that:
-                    // a) When gaining control, controller can compare with server state to evaluate
-                    // whether it needs to run animation.
-                    // b) When loosing control, controller can restore server state by taking last
-                    // dispatched state as truth.
-                    mInsetsController.onStateChanged((InsetsState) args.arg1);
-                    InsetsSourceControl[] controls = (InsetsSourceControl[]) args.arg2;
-                    if (mAdded) {
-                        mInsetsController.onControlsChanged(controls);
-                    } else if (controls != null) {
-                        for (InsetsSourceControl control : controls) {
-                            if (control != null) {
-                                control.release(SurfaceControl::release);
-                            }
-                        }
-                    }
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final InsetsState insetsState = (InsetsState) args.arg1;
+                    final InsetsSourceControl.Array activeControls =
+                            (InsetsSourceControl.Array) args.arg2;
+                    handleInsetsControlChanged(insetsState, activeControls);
                     args.recycle();
                     break;
                 }
@@ -9828,25 +9855,9 @@
         mHandler.sendMessage(msg);
     }
 
-    private void dispatchInsetsControlChanged(InsetsState insetsState,
-            InsetsSourceControl[] activeControls) {
-        if (Binder.getCallingPid() == android.os.Process.myPid()) {
-            insetsState = new InsetsState(insetsState, true /* copySource */);
-            if (activeControls != null) {
-                for (int i = activeControls.length - 1; i >= 0; i--) {
-                    activeControls[i] = new InsetsSourceControl(activeControls[i]);
-                }
-            }
-        }
-        if (mTranslator != null) {
-            mTranslator.translateInsetsStateInScreenToAppWindow(insetsState);
-            mTranslator.translateSourceControlsInScreenToAppWindow(activeControls);
-        }
-        if (insetsState != null && insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
-            ImeTracing.getInstance().triggerClientDump("ViewRootImpl#dispatchInsetsControlChanged",
-                    getInsetsController().getHost().getInputMethodManager(), null /* icProto */);
-        }
-        SomeArgs args = SomeArgs.obtain();
+    private void dispatchInsetsControlChanged(@NonNull InsetsState insetsState,
+            @NonNull InsetsSourceControl.Array activeControls) {
+        final SomeArgs args = SomeArgs.obtain();
         args.arg1 = insetsState;
         args.arg2 = activeControls;
         mHandler.obtainMessage(MSG_INSETS_CONTROL_CHANGED, args).sendToTarget();
@@ -11289,9 +11300,9 @@
                 return;
             }
             // The the parameters from WindowStateResizeItem are already copied.
-            final boolean needCopy =
+            final boolean needsCopy =
                     !isFromResizeItem && (Binder.getCallingPid() == Process.myPid());
-            if (needCopy) {
+            if (needsCopy) {
                 insetsState = new InsetsState(insetsState, true /* copySource */);
                 frames = new ClientWindowFrames(frames);
                 mergedConfiguration = new MergedConfiguration(mergedConfiguration);
@@ -11307,10 +11318,35 @@
             final boolean isFromInsetsControlChangeItem = mIsFromTransactionItem;
             mIsFromTransactionItem = false;
             final ViewRootImpl viewAncestor = mViewAncestor.get();
-            if (viewAncestor != null) {
-                viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls.get());
+            if (viewAncestor == null) {
+                if (isFromInsetsControlChangeItem) {
+                    activeControls.release();
+                }
+                return;
             }
-            // TODO(b/339380439): no need to post if the call is from InsetsControlChangeItem
+            if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
+                ImeTracing.getInstance().triggerClientDump(
+                        "ViewRootImpl#dispatchInsetsControlChanged",
+                        viewAncestor.getInsetsController().getHost().getInputMethodManager(),
+                        null /* icProto */);
+            }
+            // If the UI thread is the same as the current thread that is dispatching
+            // WindowStateInsetsControlChangeItem, then it can run directly.
+            if (isFromInsetsControlChangeItem && viewAncestor.mHandler.getLooper()
+                    == ActivityThread.currentActivityThread().getLooper()) {
+                viewAncestor.handleInsetsControlChanged(insetsState, activeControls);
+                return;
+            }
+            // The parameters from WindowStateInsetsControlChangeItem are already copied.
+            final boolean needsCopy =
+                    !isFromInsetsControlChangeItem && (Binder.getCallingPid() == Process.myPid());
+            if (needsCopy) {
+                insetsState = new InsetsState(insetsState, true /* copySource */);
+                activeControls = new InsetsSourceControl.Array(
+                        activeControls, true /* copyControls */);
+            }
+
+            viewAncestor.dispatchInsetsControlChanged(insetsState, activeControls);
         }
 
         @Override
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index 86e5bea..1af9387 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -90,6 +90,19 @@
     public static final String EXTRA_VIRTUAL_STRUCTURE_TYPE =
             "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_TYPE";
 
+
+    /**
+     * Key used for specifying the version of the view that generated the virtual structure for
+     * itself and its children
+     *
+     * For example, if the virtual structure is generated by a webview of version "104.0.5112.69",
+     * then the value should be "104.0.5112.69"
+     *
+     * @hide
+     */
+    public static final String EXTRA_VIRTUAL_STRUCTURE_VERSION_NUMBER =
+            "android.view.ViewStructure.extra.VIRTUAL_STRUCTURE_VERSION_NUMBER";
+
     /**
      * Set the identifier for this view.
      *
diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
index 12e0814..1fe8180 100644
--- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -302,6 +302,10 @@
                 }
 
                 @Override
+                public void onMagnificationSystemUIConnectionChanged(boolean connected) {
+                }
+
+                @Override
                 public void onMagnificationChanged(int displayId, @NonNull Region region,
                         MagnificationConfig config) {
                 }
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index ab7b226..edf3387 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -169,3 +169,13 @@
     description: "Feature flag for declaring system pinch zoom opt-out apis"
     bug: "315089687"
 }
+
+flag {
+    name: "wait_magnification_system_ui_connection_to_notify_service_connected"
+    namespace: "accessibility"
+    description: "Decide whether AccessibilityService needs to wait until magnification system ui connection is ready to trigger onServiceConnected"
+    bug: "337800504"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index 1bd921b..d5398e6 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -56,7 +56,16 @@
     public ClientWindowFrames() {
     }
 
-    public ClientWindowFrames(ClientWindowFrames other) {
+    public ClientWindowFrames(@NonNull ClientWindowFrames other) {
+        setTo(other);
+    }
+
+    private ClientWindowFrames(@NonNull Parcel in) {
+        readFromParcel(in);
+    }
+
+    /** Updates the current frames to the given frames. */
+    public void setTo(@NonNull ClientWindowFrames other) {
         frame.set(other.frame);
         displayFrame.set(other.displayFrame);
         parentFrame.set(other.parentFrame);
@@ -67,10 +76,6 @@
         compatScale = other.compatScale;
     }
 
-    private ClientWindowFrames(Parcel in) {
-        readFromParcel(in);
-    }
-
     /** Needed for AIDL out parameters. */
     public void readFromParcel(Parcel in) {
         frame.readFromParcel(in);
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index f928f50..4c8bad6 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -77,6 +77,14 @@
     private static final String TAG = "SnapshotDrawerUtils";
 
     /**
+     * Used to check if toolkitSetFrameRateReadOnly flag is enabled
+     *
+     * @hide
+     */
+    private static boolean sToolkitSetFrameRateReadOnlyFlagValue =
+            android.view.flags.Flags.toolkitSetFrameRateReadOnly();
+
+    /**
      * When creating the starting window, we use the exact same layout flags such that we end up
      * with a window with the exact same dimensions etc. However, these flags are not used in layout
      * and might cause other side effects so we exclude them.
@@ -439,6 +447,9 @@
         layoutParams.setFitInsetsTypes(attrs.getFitInsetsTypes());
         layoutParams.setFitInsetsSides(attrs.getFitInsetsSides());
         layoutParams.setFitInsetsIgnoringVisibility(attrs.isFitInsetsIgnoringVisibility());
+        if (sToolkitSetFrameRateReadOnlyFlagValue) {
+            layoutParams.setFrameRatePowerSavingsBalanced(false);
+        }
 
         layoutParams.setTitle(title);
         layoutParams.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
diff --git a/core/java/com/android/internal/compat/ChangeReporter.java b/core/java/com/android/internal/compat/ChangeReporter.java
index ded142c..d8f1309 100644
--- a/core/java/com/android/internal/compat/ChangeReporter.java
+++ b/core/java/com/android/internal/compat/ChangeReporter.java
@@ -98,7 +98,7 @@
     public void reportChange(int uid, long changeId, int state, boolean isLoggableBySdk) {
         boolean isAlreadyReported =
                 checkAndSetIsAlreadyReported(uid, new ChangeReport(changeId, state));
-        if (!isAlreadyReported) {
+        if (shouldWriteToStatsLog(isAlreadyReported)) {
             FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPATIBILITY_CHANGE_REPORTED, uid,
                     changeId, state, mSource);
         }
@@ -136,14 +136,16 @@
     /**
      * Returns whether the next report should be logged to FrameworkStatsLog.
      *
-     * @param uid      affected by the change
-     * @param changeId the reported change id
-     * @param state    of the reported change - enabled/disabled/only logged
+     * @param isAlreadyReported is the change already reported
      * @return true if the report should be logged
      */
     @VisibleForTesting
-    boolean shouldWriteToStatsLog(int uid, long changeId, int state) {
-        return !isAlreadyReported(uid, new ChangeReport(changeId, state));
+    boolean shouldWriteToStatsLog(boolean isAlreadyReported) {
+        // We don't log for system server
+        if (mSource == SOURCE_SYSTEM_SERVER) return false;
+
+        // Don't log if already reported
+        return !isAlreadyReported;
     }
 
     /**
@@ -160,8 +162,6 @@
             boolean isAlreadyReported, int state, boolean isLoggableBySdk) {
         // If log all bit is on, always return true.
         if (mDebugLogAll) return true;
-        // If the change has already been reported, do not write.
-        if (isAlreadyReported) return false;
 
         // If the flag is turned off or the TAG's logging is forced to debug level with
         // `adb setprop log.tag.CompatChangeReporter=DEBUG`, write to debug since the above checks
@@ -224,6 +224,19 @@
         return mReportedChanges.getOrDefault(uid, EMPTY_SET).contains(report);
     }
 
+    /**
+     * Returns whether the next report should be logged.
+     *
+     * @param uid      affected by the change
+     * @param changeId the reported change id
+     * @param state    of the reported change - enabled/disabled/only logged
+     * @return true if the report should be logged
+     */
+    @VisibleForTesting
+    boolean isAlreadyReported(int uid, long changeId, int state) {
+        return isAlreadyReported(uid, new ChangeReport(changeId, state));
+    }
+
     private void markAsReported(int uid, ChangeReport report) {
         mReportedChanges.computeIfAbsent(uid, NEW_CHANGE_REPORT_SET).add(report);
     }
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 37b7288..42fa6ac 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -135,7 +135,7 @@
                 new DataSourceParams.Builder()
                         .setBufferExhaustedPolicy(
                                 DataSourceParams
-                                        .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT)
+                                        .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
                         .build();
         mDataSource.register(params);
         this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider;
diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/core/java/com/android/internal/util/NewlineNormalizer.java
deleted file mode 100644
index 0104d1f..0000000
--- a/core/java/com/android/internal/util/NewlineNormalizer.java
+++ /dev/null
@@ -1,39 +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.internal.util;
-
-
-import java.util.regex.Pattern;
-
-/**
- * Utility class that replaces consecutive empty lines with single new line.
- * @hide
- */
-public class NewlineNormalizer {
-
-    private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
-
-    // Private constructor to prevent instantiation
-    private NewlineNormalizer() {}
-
-    /**
-     * Replaces consecutive newlines with a single newline in the input text.
-     */
-    public static String normalizeNewlines(String text) {
-        return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
-    }
-}
diff --git a/core/java/com/android/internal/util/NotificationBigTextNormalizer.java b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java
new file mode 100644
index 0000000..80d4095
--- /dev/null
+++ b/core/java/com/android/internal/util/NotificationBigTextNormalizer.java
@@ -0,0 +1,123 @@
+/*
+ * 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.internal.util;
+
+
+import android.annotation.NonNull;
+import android.os.Trace;
+
+import java.util.regex.Pattern;
+
+/**
+ * Utility class that normalizes BigText style Notification content.
+ * @hide
+ */
+public class NotificationBigTextNormalizer {
+
+    private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
+    private static final Pattern HORIZONTAL_WHITESPACES = Pattern.compile("\\h+");
+
+    // Private constructor to prevent instantiation
+    private NotificationBigTextNormalizer() {}
+
+    /**
+     * Normalizes the given text by collapsing consecutive new lines into single one and cleaning
+     * up each line by removing zero-width characters, invisible formatting characters, and
+     * collapsing consecutive whitespace into single space.
+     */
+    @NonNull
+    public static String normalizeBigText(@NonNull String text) {
+        try {
+            Trace.beginSection("NotifBigTextNormalizer#normalizeBigText");
+            text = MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
+            text = HORIZONTAL_WHITESPACES.matcher(text).replaceAll(" ");
+            text = normalizeLines(text);
+            return text;
+        } finally {
+            Trace.endSection();
+        }
+    }
+
+    /**
+     * Normalizes lines in a text by removing zero-width characters, invisible formatting
+     * characters, and collapsing consecutive whitespace into single space.
+     *
+     * <p>
+     * This method processes the input text line by line. It eliminates zero-width
+     * characters (U+200B to U+200D, U+FEFF, U+034F), invisible formatting
+     * characters (U+2060 to U+2065, U+206A to U+206F, U+FFF9 to U+FFFB),
+     * and replaces any sequence of consecutive whitespace characters with a single space.
+     * </p>
+     *
+     * <p>
+     * Additionally, the method trims trailing whitespace from each line and removes any
+     * resulting empty lines.
+     * </p>
+     */
+    @NonNull
+    private static String normalizeLines(@NonNull String text) {
+        String[] lines = text.split("\n");
+        final StringBuilder textSB = new StringBuilder(text.length());
+        for (int i = 0; i < lines.length; i++) {
+            final String line = lines[i];
+            final StringBuilder lineSB = new StringBuilder(line.length());
+            boolean spaceSeen = false;
+            for (int j = 0; j < line.length(); j++) {
+                final char character = line.charAt(j);
+
+                // Skip ZERO WIDTH characters
+                if ((character >= '\u200B' && character <= '\u200D')
+                        || character == '\uFEFF' || character == '\u034F') {
+                    continue;
+                }
+                // Skip INVISIBLE_FORMATTING_CHARACTERS
+                if ((character >= '\u2060' && character <= '\u2065')
+                        || (character >= '\u206A' && character <= '\u206F')
+                        || (character >= '\uFFF9' && character <= '\uFFFB')) {
+                    continue;
+                }
+
+                if (isSpace(character)) {
+                    // eliminate consecutive spaces....
+                    if (!spaceSeen) {
+                        lineSB.append(" ");
+                    }
+                    spaceSeen = true;
+                } else {
+                    spaceSeen = false;
+                    lineSB.append(character);
+                }
+            }
+            // trim line.
+            final String currentLine = lineSB.toString().trim();
+
+            // don't add empty lines after trim.
+            if (currentLine.length() > 0) {
+                if (textSB.length() > 0) {
+                    textSB.append("\n");
+                }
+                textSB.append(currentLine);
+            }
+        }
+
+        return textSB.toString();
+    }
+
+    private static boolean isSpace(char ch) {
+        return ch != '\n' && Character.isSpaceChar(ch);
+    }
+}
diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java
index 0f4615a..58bddae 100644
--- a/core/java/com/android/internal/widget/NotificationRowIconView.java
+++ b/core/java/com/android/internal/widget/NotificationRowIconView.java
@@ -59,7 +59,7 @@
     @Override
     protected void onFinishInflate() {
         // If showing the app icon, we don't need background or padding.
-        if (Flags.notificationsUseAppIcon()) {
+        if (Flags.notificationsUseAppIcon() || Flags.notificationsUseAppIconInRow()) {
             setPadding(0, 0, 0, 0);
             setBackground(null);
         }
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index d48cdc4..eaff760 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -713,6 +713,19 @@
             AudioSystem::getForceUse(static_cast<audio_policy_force_use_t>(usage)));
 }
 
+static jint android_media_AudioSystem_setDeviceAbsoluteVolumeEnabled(JNIEnv *env, jobject thiz,
+                                                                     jint device, jstring address,
+                                                                     jboolean enabled,
+                                                                     jint stream) {
+    const char *c_address = env->GetStringUTFChars(address, nullptr);
+    int state = check_AudioSystem_Command(
+            AudioSystem::setDeviceAbsoluteVolumeEnabled(static_cast<audio_devices_t>(device),
+                                                        c_address, enabled,
+                                                        static_cast<audio_stream_type_t>(stream)));
+    env->ReleaseStringUTFChars(address, c_address);
+    return state;
+}
+
 static jint
 android_media_AudioSystem_initStreamVolume(JNIEnv *env, jobject thiz, jint stream, jint indexMin, jint indexMax)
 {
@@ -3373,6 +3386,7 @@
          MAKE_AUDIO_SYSTEM_METHOD(setPhoneState),
          MAKE_AUDIO_SYSTEM_METHOD(setForceUse),
          MAKE_AUDIO_SYSTEM_METHOD(getForceUse),
+         MAKE_AUDIO_SYSTEM_METHOD(setDeviceAbsoluteVolumeEnabled),
          MAKE_AUDIO_SYSTEM_METHOD(initStreamVolume),
          MAKE_AUDIO_SYSTEM_METHOD(setStreamVolumeIndex),
          MAKE_AUDIO_SYSTEM_METHOD(getStreamVolumeIndex),
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
index f8c63c8..f553380 100644
--- a/core/jni/android_tracing_PerfettoProducer.cpp
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -34,15 +34,17 @@
 
 namespace android {
 
-void perfettoProducerInit(JNIEnv* env, jclass clazz, int backends) {
+void perfettoProducerInit(JNIEnv* env, jclass clazz, PerfettoBackendTypes backends,
+                          uint32_t shmem_size_hint_kb) {
     struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
-    args.backends = (PerfettoBackendTypes)backends;
+    args.backends = backends;
+    args.shmem_size_hint_kb = shmem_size_hint_kb;
     PerfettoProducerInit(args);
 }
 
 const JNINativeMethod gMethods[] = {
         /* name, signature, funcPtr */
-        {"nativePerfettoProducerInit", "(I)V", (void*)perfettoProducerInit},
+        {"nativePerfettoProducerInit", "(II)V", (void*)perfettoProducerInit},
 };
 
 int register_android_tracing_PerfettoProducer(JNIEnv* env) {
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 3006e20..2068bd7 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -1411,10 +1411,8 @@
         return JNI_TRUE;
     }
 
-    if (err == FAILED_TRANSACTION) {
-        env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback,
-                                  getpid(), code, flags, err);
-    }
+    env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
+                              code, flags, err);
 
     if (err == UNKNOWN_TRANSACTION) {
         return JNI_FALSE;
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp
index 31f4e64..d426f12 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.cpp
+++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp
@@ -88,7 +88,7 @@
         ALOGD("Total number of LOAD segments %zu", programHeaders.size());
 
         ALOGD("Size before punching holes st_blocks: %" PRIu64
-              ", st_blksize: %ld, st_size: %" PRIu64 "",
+              ", st_blksize: %d, st_size: %" PRIu64 "",
               beforePunch.st_blocks, beforePunch.st_blksize,
               static_cast<uint64_t>(beforePunch.st_size));
     }
@@ -193,7 +193,7 @@
             ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno);
             return false;
         }
-        ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %ld, st_size: %" PRIu64
+        ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %d, st_size: %" PRIu64
               "",
               afterPunch.st_blocks, afterPunch.st_blksize,
               static_cast<uint64_t>(afterPunch.st_size));
@@ -271,7 +271,7 @@
     uint64_t blockSize = beforePunch.st_blksize;
     IF_ALOGD() {
         ALOGD("Extra field length: %hu,  Size before punching holes st_blocks: %" PRIu64
-              ", st_blksize: %ld, st_size: %" PRIu64 "",
+              ", st_blksize: %d, st_size: %" PRIu64 "",
               extraFieldLen, beforePunch.st_blocks, beforePunch.st_blksize,
               static_cast<uint64_t>(beforePunch.st_size));
     }
@@ -346,7 +346,7 @@
             return false;
         }
         ALOGD("punchHolesInApk:: Size after punching holes st_blocks: %" PRIu64
-              ", st_blksize: %ld, st_size: %" PRIu64 "",
+              ", st_blksize: %d, st_size: %" PRIu64 "",
               afterPunch.st_blocks, afterPunch.st_blksize,
               static_cast<uint64_t>(afterPunch.st_size));
     }
diff --git a/core/proto/android/app/appexitinfo.proto b/core/proto/android/app/appexitinfo.proto
index 3abc462..e560a94 100644
--- a/core/proto/android/app/appexitinfo.proto
+++ b/core/proto/android/app/appexitinfo.proto
@@ -20,7 +20,7 @@
 package android.app;
 
 import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
 
 /**
  * An android.app.ApplicationExitInfo object.
diff --git a/core/proto/android/app/appstartinfo.proto b/core/proto/android/app/appstartinfo.proto
index d9ed911..c137533 100644
--- a/core/proto/android/app/appstartinfo.proto
+++ b/core/proto/android/app/appstartinfo.proto
@@ -20,7 +20,7 @@
 package android.app;
 
 import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
 
 /**
  * An android.app.ApplicationStartInfo object.
diff --git a/core/proto/android/os/batterystats.proto b/core/proto/android/os/batterystats.proto
index 4c84944..97f8148 100644
--- a/core/proto/android/os/batterystats.proto
+++ b/core/proto/android/os/batterystats.proto
@@ -21,7 +21,7 @@
 
 import "frameworks/base/core/proto/android/os/powermanager.proto";
 import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/job/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto";
 import "frameworks/proto_logging/stats/enums/telephony/enums.proto";
 
 message BatteryStatsProto {
diff --git a/core/proto/android/server/activitymanagerservice.proto b/core/proto/android/server/activitymanagerservice.proto
index e3a438d..921c41c 100644
--- a/core/proto/android/server/activitymanagerservice.proto
+++ b/core/proto/android/server/activitymanagerservice.proto
@@ -35,7 +35,7 @@
 import "frameworks/base/core/proto/android/server/windowmanagerservice.proto";
 import "frameworks/base/core/proto/android/util/common.proto";
 import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
 
 option java_multiple_files = true;
 
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 00127c1..a1e3dc1 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -31,7 +31,7 @@
 import "frameworks/base/core/proto/android/server/statlogger.proto";
 import "frameworks/base/core/proto/android/privacy.proto";
 import "frameworks/base/core/proto/android/util/quotatracker.proto";
-import "frameworks/proto_logging/stats/enums/app/job/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/job/job_enums.proto";
 import "frameworks/proto_logging/stats/enums/server/job/enums.proto";
 
 message JobSchedulerServiceDumpProto {
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index 2f865af..593bbc6 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -26,7 +26,7 @@
 import "frameworks/base/core/proto/android/providers/settings.proto";
 import "frameworks/base/core/proto/android/server/wirelesschargerdetector.proto";
 import "frameworks/base/core/proto/android/privacy.proto";
-import "frameworks/proto_logging/stats/enums/app/enums.proto";
+import "frameworks/proto_logging/stats/enums/app/app_enums.proto";
 import "frameworks/proto_logging/stats/enums/os/enums.proto";
 import "frameworks/proto_logging/stats/enums/view/enums.proto";
 
diff --git a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
index 12a42f9..c32fd45 100644
--- a/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
+++ b/core/tests/PlatformCompatFramework/src/com/android/internal/compat/ChangeReporterTest.java
@@ -31,21 +31,21 @@
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     @Test
-    public void testStatsLogOnce() {
+    public void testIsAlreadyReportedOnce() {
         ChangeReporter reporter = new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
         int myUid = 1022, otherUid = 1023;
         long myChangeId = 500L, otherChangeId = 600L;
         int myState = ChangeReporter.STATE_ENABLED, otherState = ChangeReporter.STATE_DISABLED;
 
-        assertTrue(reporter.shouldWriteToStatsLog(myUid, myChangeId, myState));
+        assertFalse(reporter.isAlreadyReported(myUid, myChangeId, myState));
         reporter.reportChange(myUid, myChangeId, myState);
 
         // Same report will not be logged again.
-        assertFalse(reporter.shouldWriteToStatsLog(myUid, myChangeId, myState));
+        assertTrue(reporter.isAlreadyReported(myUid, myChangeId, myState));
         // Other reports will be logged.
-        assertTrue(reporter.shouldWriteToStatsLog(otherUid, myChangeId, myState));
-        assertTrue(reporter.shouldWriteToStatsLog(myUid, otherChangeId, myState));
-        assertTrue(reporter.shouldWriteToStatsLog(myUid, myChangeId, otherState));
+        assertFalse(reporter.isAlreadyReported(otherUid, myChangeId, myState));
+        assertFalse(reporter.isAlreadyReported(myUid, otherChangeId, myState));
+        assertFalse(reporter.isAlreadyReported(myUid, myChangeId, otherState));
     }
 
     @Test
@@ -55,15 +55,15 @@
         long myChangeId = 500L;
         int myState = ChangeReporter.STATE_ENABLED;
 
-        assertTrue(reporter.shouldWriteToStatsLog(myUid, myChangeId, myState));
+        assertFalse(reporter.isAlreadyReported(myUid, myChangeId, myState));
         reporter.reportChange(myUid, myChangeId, myState);
 
         // Same report will not be logged again.
-        assertFalse(reporter.shouldWriteToStatsLog(myUid, myChangeId, myState));
+        assertTrue(reporter.isAlreadyReported(myUid, myChangeId, myState));
         reporter.resetReportedChanges(myUid);
 
         // Same report will be logged again after reset.
-        assertTrue(reporter.shouldWriteToStatsLog(myUid, myChangeId, myState));
+        assertFalse(reporter.isAlreadyReported(myUid, myChangeId, myState));
     }
 
     @Test
@@ -196,4 +196,21 @@
         // off.
         assertTrue(reporter.shouldWriteToDebug(myUid, myChangeId, myDisabledState, false));
     }
+
+    @Test
+    public void testDontLogSystemServer() {
+        ChangeReporter systemServerReporter =
+                new ChangeReporter(ChangeReporter.SOURCE_SYSTEM_SERVER);
+
+        // We never log from system server, even if already reported.
+        assertFalse(systemServerReporter.shouldWriteToStatsLog(false));
+        assertFalse(systemServerReporter.shouldWriteToStatsLog(true));
+
+        ChangeReporter unknownSourceReporter =
+                new ChangeReporter(ChangeReporter.SOURCE_UNKNOWN_SOURCE);
+
+        // Otherwise we log if it's not already been reported.
+        assertTrue(unknownSourceReporter.shouldWriteToStatsLog(false));
+        assertFalse(unknownSourceReporter.shouldWriteToStatsLog(true));
+    }
 }
diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
index 89c2b3c..b972882 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
@@ -128,7 +128,8 @@
                 0x222222,                // colorBackground
                 0x333333,                // statusBarColor
                 0x444444,                // navigationBarColor
-                0,                       // statusBarAppearance
+                0x555555,                // systemBarsAppeareance
+                0x666666,                // topOpaqueSystemBarsAppeareance
                 true,                    // ensureStatusBarContrastWhenTransparent
                 true,                    // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_RESIZEABLE,  // resizeMode
@@ -153,7 +154,8 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
-                0,                         // statusBarAppearance
+                0x555555,                  // systemBarsAppeareance
+                0x666666,                  // topOpaqueSystemBarsAppeareance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -169,7 +171,8 @@
                 0x2222222,               // colorBackground
                 0x3333332,               // statusBarColor
                 0x4444442,               // navigationBarColor
-                0,                       // statusBarAppearance
+                0x5555552,               // systemBarsAppeareance
+                0x6666662,               // topOpaqueSystemBarsAppeareance
                 true,                    // ensureStatusBarContrastWhenTransparent
                 true,                    // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_RESIZEABLE,  // resizeMode
@@ -200,7 +203,8 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
-                0,                         // statusBarAppearance
+                0x555555,                  // systemBarsAppeareance
+                0x666666,                  // topOpaqueSystemBarsAppeareance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -223,7 +227,8 @@
                 0x222222,                  // colorBackground
                 0x333333,                  // statusBarColor
                 0x444444,                  // navigationBarColor
-                0,                         // statusBarAppearance
+                0x555555,                  // systemBarsAppeareance
+                0x666666,                  // topOpaqueSystemBarsAppeareance
                 false,                     // ensureStatusBarContrastWhenTransparent
                 false,                     // ensureNavigationBarContrastWhenTransparent
                 RESIZE_MODE_UNRESIZEABLE,  // resizeMode
@@ -256,6 +261,8 @@
             assertEquals(td1.getStatusBarColor(), td2.getStatusBarColor());
             assertEquals(td1.getNavigationBarColor(), td2.getNavigationBarColor());
             assertEquals(td1.getSystemBarsAppearance(), td2.getSystemBarsAppearance());
+            assertEquals(td1.getTopOpaqueSystemBarsAppearance(),
+                    td2.getTopOpaqueSystemBarsAppearance());
             assertEquals(td1.getResizeMode(), td2.getResizeMode());
             assertEquals(td1.getMinWidth(), td2.getMinWidth());
             assertEquals(td1.getMinHeight(), td2.getMinHeight());
diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
index 3735274..c7060ad 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java
@@ -23,6 +23,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
 import android.app.Activity;
@@ -39,7 +41,6 @@
 import android.view.InsetsState;
 import android.window.ActivityWindowInfo;
 import android.window.ClientWindowFrames;
-import android.window.WindowContext;
 import android.window.WindowContextInfo;
 
 import androidx.test.filters.SmallTest;
@@ -73,8 +74,6 @@
     @Mock
     private IBinder mWindowClientToken;
     @Mock
-    private WindowContext mWindowContext;
-    @Mock
     private IWindow mWindow;
 
     // Can't mock final class.
@@ -176,4 +175,17 @@
 
         verify(mWindow).insetsControlChanged(mInsetsState, mActiveControls);
     }
+
+    @Test
+    public void testWindowStateInsetsControlChangeItem_executeError() throws RemoteException {
+        doThrow(new RemoteException()).when(mWindow).insetsControlChanged(any(), any());
+
+        mActiveControls = spy(mActiveControls);
+        final WindowStateInsetsControlChangeItem item = WindowStateInsetsControlChangeItem.obtain(
+                mWindow, mInsetsState, mActiveControls);
+        item.mActiveControls = mActiveControls;
+        item.execute(mHandler, mPendingActions);
+
+        verify(mActiveControls).release();
+    }
 }
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 1c12362..98f8b7f 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -39,6 +39,7 @@
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.text.Layout.Alignment;
+import android.text.style.ForegroundColorSpan;
 import android.text.style.StrikethroughSpan;
 
 import androidx.test.filters.SmallTest;
@@ -933,6 +934,83 @@
         expect.that(numBackgroundsFound).isEqualTo(backgroundRectsDrawn);
     }
 
+    @Test
+    @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+    public void highContrastTextEnabled_testDrawMulticolorText_drawsBlackAndWhiteBackgrounds() {
+        /*
+        Here's what the final render should look like:
+
+       Text  |   Background
+     ========================
+        al   |    BW
+        w    |    WW
+        ei   |    WW
+        \t;  |    WW
+        s    |    BB
+        df   |    BB
+        s    |    BB
+        df   |    BB
+        @    |    BB
+      ------------------------
+         */
+
+        mTextPaint.setColor(Color.WHITE);
+
+        mSpannedText.setSpan(
+                // Can't use DKGREY because it is right on the cusp of clamping white
+                new ForegroundColorSpan(0xFF332211),
+                /* start= */ 1,
+                /* end= */ 6,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        );
+        mSpannedText.setSpan(
+                new ForegroundColorSpan(Color.LTGRAY),
+                /* start= */ 8,
+                /* end= */ 11,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        );
+        Layout layout = new StaticLayout(mSpannedText, mTextPaint, mWidth,
+                mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+        final int width = 256;
+        final int height = 256;
+        MockCanvas c = new MockCanvas(width, height);
+        c.setHighContrastTextEnabled(true);
+        layout.draw(
+                c,
+                /* highlightPaths= */ null,
+                /* highlightPaints= */ null,
+                /* selectionPath= */ null,
+                /* selectionPaint= */ null,
+                /* cursorOffsetVertical= */ 0
+        );
+        List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+        var highlightsDrawn = 0;
+        var numColorChangesWithinOneLine = 1;
+        var textsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine;
+        var backgroundRectsDrawn = STATIC_LINE_COUNT + numColorChangesWithinOneLine;
+        expect.withMessage("wrong number of drawCommands: " + drawCommands)
+                .that(drawCommands.size())
+                .isEqualTo(textsDrawn + backgroundRectsDrawn + highlightsDrawn);
+
+        var backgroundCommands = drawCommands.stream()
+                .filter(it -> it.rect != null)
+                .toList();
+
+        expect.that(backgroundCommands.get(0).paint.getColor()).isEqualTo(Color.BLACK);
+        expect.that(backgroundCommands.get(1).paint.getColor()).isEqualTo(Color.WHITE);
+        expect.that(backgroundCommands.get(2).paint.getColor()).isEqualTo(Color.WHITE);
+        expect.that(backgroundCommands.get(3).paint.getColor()).isEqualTo(Color.WHITE);
+        expect.that(backgroundCommands.get(4).paint.getColor()).isEqualTo(Color.WHITE);
+        expect.that(backgroundCommands.get(5).paint.getColor()).isEqualTo(Color.BLACK);
+        expect.that(backgroundCommands.get(6).paint.getColor()).isEqualTo(Color.BLACK);
+        expect.that(backgroundCommands.get(7).paint.getColor()).isEqualTo(Color.BLACK);
+        expect.that(backgroundCommands.get(8).paint.getColor()).isEqualTo(Color.BLACK);
+        expect.that(backgroundCommands.get(9).paint.getColor()).isEqualTo(Color.BLACK);
+
+        expect.that(backgroundCommands.size()).isEqualTo(backgroundRectsDrawn);
+    }
+
     private static final class MockCanvas extends Canvas {
 
         static class DrawCommand {
diff --git a/core/tests/coretests/src/android/text/SpanColorsTest.java b/core/tests/coretests/src/android/text/SpanColorsTest.java
new file mode 100644
index 0000000..3d8d8f9
--- /dev/null
+++ b/core/tests/coretests/src/android/text/SpanColorsTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.text;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Color;
+import android.graphics.drawable.ShapeDrawable;
+import android.platform.test.annotations.Presubmit;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.UnderlineSpan;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SpanColorsTest {
+    private final TextPaint mWorkPaint = new TextPaint();
+    private SpanColors mSpanColors;
+    private SpannableString mSpannedText;
+
+    @Before
+    public void setup() {
+        mSpanColors = new SpanColors();
+        mSpannedText = new SpannableString("Hello world! This is a test.");
+        mSpannedText.setSpan(new ForegroundColorSpan(Color.RED), 0, 4,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        mSpannedText.setSpan(new ForegroundColorSpan(Color.GREEN), 6, 11,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        mSpannedText.setSpan(new UnderlineSpan(), 5, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        mSpannedText.setSpan(new ImageSpan(new ShapeDrawable()), 1, 2,
+                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        mSpannedText.setSpan(new ForegroundColorSpan(Color.BLUE), 12, 16,
+                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+    }
+
+    @Test
+    public void testNoColorFound() {
+        mSpanColors.init(mWorkPaint, mSpannedText, 25, 30); // Beyond the spans
+        assertThat(mSpanColors.getColorAt(27)).isEqualTo(SpanColors.NO_COLOR_FOUND);
+    }
+
+    @Test
+    public void testSingleColorSpan() {
+        mSpanColors.init(mWorkPaint, mSpannedText, 1, 4);
+        assertThat(mSpanColors.getColorAt(3)).isEqualTo(Color.RED);
+    }
+
+    @Test
+    public void testMultipleColorSpans() {
+        mSpanColors.init(mWorkPaint, mSpannedText, 0, mSpannedText.length());
+        assertThat(mSpanColors.getColorAt(2)).isEqualTo(Color.RED);
+        assertThat(mSpanColors.getColorAt(5)).isEqualTo(SpanColors.NO_COLOR_FOUND);
+        assertThat(mSpanColors.getColorAt(8)).isEqualTo(Color.GREEN);
+        assertThat(mSpanColors.getColorAt(13)).isEqualTo(Color.BLUE);
+    }
+}
diff --git a/core/tests/coretests/src/android/util/BinaryXmlTest.java b/core/tests/coretests/src/android/util/BinaryXmlTest.java
index 025e831..da29828 100644
--- a/core/tests/coretests/src/android/util/BinaryXmlTest.java
+++ b/core/tests/coretests/src/android/util/BinaryXmlTest.java
@@ -24,6 +24,8 @@
 import static android.util.XmlTest.doVerifyWrite;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
 import static org.xmlpull.v1.XmlPullParser.START_TAG;
 
 import android.os.PersistableBundle;
@@ -41,12 +43,15 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 
 @RunWith(AndroidJUnit4.class)
 public class BinaryXmlTest {
+    private static final int MAX_UNSIGNED_SHORT = 65_535;
+
     /**
      * Verify that we can write and read large numbers of interned
      * {@link String} values.
@@ -170,4 +175,49 @@
             }
         }
     }
+
+    @Test
+    public void testAttributeBytes_BinaryDataOverflow() throws Exception {
+        final TypedXmlSerializer out = Xml.newBinarySerializer();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+
+        final byte[] testBytes = new byte[MAX_UNSIGNED_SHORT + 1];
+        assertThrows(IOException.class,
+                () -> out.attributeBytesHex(/* namespace */ null, /* name */ "attributeBytesHex",
+                        testBytes));
+
+        assertThrows(IOException.class,
+                () -> out.attributeBytesBase64(/* namespace */ null, /* name */
+                        "attributeBytesBase64", testBytes));
+    }
+
+    @Test
+    public void testAttributeBytesHex_MaximumBinaryData() throws Exception {
+        final TypedXmlSerializer out = Xml.newBinarySerializer();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+
+        final byte[] testBytes = new byte[MAX_UNSIGNED_SHORT];
+        try {
+            out.attributeBytesHex(/* namespace */ null, /* name */ "attributeBytesHex", testBytes);
+        } catch (Exception e) {
+            fail("testAttributeBytesHex fails with exception: " + e.toString());
+        }
+    }
+
+    @Test
+    public void testAttributeBytesBase64_MaximumBinaryData() throws Exception {
+        final TypedXmlSerializer out = Xml.newBinarySerializer();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        out.setOutput(os, StandardCharsets.UTF_8.name());
+
+        final byte[] testBytes = new byte[MAX_UNSIGNED_SHORT];
+        try {
+            out.attributeBytesBase64(/* namespace */ null, /* name */ "attributeBytesBase64",
+                    testBytes);
+        } catch (Exception e) {
+            fail("testAttributeBytesBase64 fails with exception: " + e.toString());
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 07446e7..abe9c8e 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -164,7 +164,7 @@
         mActivityRule.runOnUiThread(() -> {
             mMovingView.setFrameContentVelocity(1f);
             mMovingView.invalidate();
-            runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
+            runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
         });
         waitForAfterDraw();
     }
@@ -190,7 +190,7 @@
             frameLayout.setFrameContentVelocity(1f);
             mMovingView.offsetTopAndBottom(100);
             frameLayout.invalidate();
-            runAfterDraw(() -> assertEquals(60f, mViewRoot.getLastPreferredFrameRate(), 0f));
+            runAfterDraw(() -> assertEquals(80f, mViewRoot.getLastPreferredFrameRate(), 0f));
         });
         waitForAfterDraw();
     }
@@ -435,7 +435,7 @@
             runAfterDraw(() -> {
                 assertEquals(FRAME_RATE_CATEGORY_LOW,
                         mViewRoot.getLastPreferredFrameRateCategory());
-                assertEquals(60f, mViewRoot.getLastPreferredFrameRate());
+                assertEquals(80f, mViewRoot.getLastPreferredFrameRate());
             });
         });
         waitForAfterDraw();
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index a7f8176..94e187a 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1287,6 +1287,31 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY)
+    public void votePreferredFrameRate_velocityVotedAfterOnDraw() throws Throwable {
+        mView = new View(sContext);
+        double delta = 0.1;
+        float pixelsPerSecond = 1000_000;
+        float expectedFrameRate = 120;
+        attachViewToWindow(mView);
+        sInstrumentation.waitForIdleSync();
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        waitForFrameRateCategoryToSettle(mView);
+
+        sInstrumentation.runOnMainSync(() -> {
+            mView.setFrameContentVelocity(pixelsPerSecond);
+            mView.invalidate();
+            assertEquals(0, viewRoot.getPreferredFrameRate(), delta);
+            assertEquals(0, viewRoot.getLastPreferredFrameRate(), delta);
+            runAfterDraw(() -> {
+                assertEquals(expectedFrameRate, viewRoot.getPreferredFrameRate(), delta);
+                assertEquals(expectedFrameRate, viewRoot.getLastPreferredFrameRate(), delta);
+            });
+        });
+        waitForAfterDraw();
+    }
+
+    @Test
     public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() {
         mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
         ShellIdentityUtils.invokeWithShellPermissions(() -> {
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index b5c264c..5a6824b 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -146,6 +146,10 @@
 
     public void setMagnificationCallbackEnabled(int displayId, boolean enabled) {}
 
+    public boolean isMagnificationSystemUIConnected() {
+        return false;
+    }
+
     public boolean setSoftKeyboardShowMode(int showMode) {
         return false;
     }
diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
deleted file mode 100644
index bcdac61..0000000
--- a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
+++ /dev/null
@@ -1,71 +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.internal.util;
-
-import static junit.framework.Assert.assertEquals;
-
-
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Test for {@link NewlineNormalizer}
- * @hide
- */
-@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class)
-@RunWith(AndroidJUnit4.class)
-public class NewlineNormalizerTest {
-
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Test
-    public void testEmptyInput() {
-        assertEquals("", NewlineNormalizer.normalizeNewlines(""));
-    }
-
-    @Test
-    public void testSingleNewline() {
-        assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n"));
-    }
-
-    @Test
-    public void testMultipleConsecutiveNewlines() {
-        assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n"));
-    }
-
-    @Test
-    public void testNewlinesWithSpacesAndTabs() {
-        String input = "Line 1\n  \n \t \n\tLine 2";
-        // Adjusted expected output to include the tab character
-        String expected = "Line 1\n\tLine 2";
-        assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
-    }
-
-    @Test
-    public void testMixedNewlineCharacters() {
-        String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
-        String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
-        assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
-    }
-}
diff --git a/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java
new file mode 100644
index 0000000..1f2e24a
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/NotificationBigTextNormalizerTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.internal.util;
+
+import static junit.framework.Assert.assertEquals;
+
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link NotificationBigTextNormalizer}
+ * @hide
+ */
+@DisabledOnRavenwood(blockedBy = NotificationBigTextNormalizer.class)
+@RunWith(AndroidJUnit4.class)
+public class NotificationBigTextNormalizerTest {
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+
+    @Test
+    public void testEmptyInput() {
+        assertEquals("", NotificationBigTextNormalizer.normalizeBigText(""));
+    }
+
+    @Test
+    public void testSingleNewline() {
+        assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n"));
+    }
+
+    @Test
+    public void testMultipleConsecutiveNewlines() {
+        assertEquals("", NotificationBigTextNormalizer.normalizeBigText("\n\n\n\n\n"));
+    }
+
+    @Test
+    public void testNewlinesWithSpacesAndTabs() {
+        String input = "Line 1\n  \n \t \n\tLine 2";
+        // Adjusted expected output to include the tab character
+        String expected = "Line 1\nLine 2";
+        assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+    }
+
+    @Test
+    public void testMixedNewlineCharacters() {
+        String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
+        String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
+        assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+    }
+
+    @Test
+    public void testConsecutiveSpaces() {
+        // Only spaces
+        assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+                + "              is   a                         test."));
+        // Zero width characters bw spaces.
+        assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+                + "\u200B \u200B \u200B \u200B \u200B \u200B \u200B \u200Bis\uFEFF \uFEFF \uFEFF"
+                + " \uFEFFa \u034F \u034F \u034F \u034F \u034F \u034Ftest."));
+
+        // Invisible formatting characters bw spaces.
+        assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+                + "\u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061 \u2061is\u206E \u206E \u206E"
+                + " \u206Ea \uFFFB \uFFFB \uFFFB \uFFFB \uFFFB \uFFFBtest."));
+        // Non breakable spaces
+        assertEquals("This is a test.", NotificationBigTextNormalizer.normalizeBigText("This"
+                + "\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0is\u2005 \u2005 \u2005"
+                + " \u2005a\u2005\u2005\u2005 \u2005\u2005\u2005test."));
+    }
+
+    @Test
+    public void testZeroWidthCharRemoval() {
+        // Test each character individually
+        char[] zeroWidthChars = { '\u200B', '\u200C', '\u200D', '\uFEFF', '\u034F' };
+
+        for (char c : zeroWidthChars) {
+            String input = "Test" + c + "string";
+            String expected = "Teststring";
+            assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+        }
+    }
+
+    @Test
+    public void testWhitespaceReplacement() {
+        assertEquals("This text has horizontal whitespace.",
+                NotificationBigTextNormalizer.normalizeBigText(
+                        "This\ttext\thas\thorizontal\twhitespace."));
+        assertEquals("This text has mixed whitespace.",
+                NotificationBigTextNormalizer.normalizeBigText(
+                        "This  text  has \u00A0 mixed\u2009whitespace."));
+        assertEquals("This text has leading and trailing whitespace.",
+                NotificationBigTextNormalizer.normalizeBigText(
+                        "\t This text has leading and trailing whitespace. \n"));
+    }
+
+    @Test
+    public void testInvisibleFormattingCharacterRemoval() {
+        // Test each character individually
+        char[] invisibleFormattingChars = {
+                '\u2060', '\u2061', '\u2062', '\u2063', '\u2064', '\u2065',
+                '\u206A', '\u206B', '\u206C', '\u206D', '\u206E', '\u206F',
+                '\uFFF9', '\uFFFA', '\uFFFB'
+        };
+
+        for (char c : invisibleFormattingChars) {
+            String input = "Test " + c + "string";
+            String expected = "Test string";
+            assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+        }
+    }
+    @Test
+    public void testNonBreakSpaceReplacement() {
+        // Test each character individually
+        char[] nonBreakSpaces = {
+                '\u00A0', '\u1680', '\u2000', '\u2001', '\u2002',
+                '\u2003', '\u2004', '\u2005', '\u2006', '\u2007',
+                '\u2008', '\u2009', '\u200A', '\u202F', '\u205F', '\u3000'
+        };
+
+        for (char c : nonBreakSpaces) {
+            String input = "Test" + c + "string";
+            String expected = "Test string";
+            assertEquals(expected, NotificationBigTextNormalizer.normalizeBigText(input));
+        }
+    }
+}
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index d359a90..3cff915 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -1149,6 +1149,8 @@
 
         /**
          * Sets the serial number used for the certificate of the generated key pair.
+         * To ensure compatibility with devices and certificate parsers, the value
+         * should be 20 bytes or shorter (see RFC 5280 section 4.1.2.2).
          *
          * <p>By default, the serial number is {@code 1}.
          */
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 774b212..23dc96c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -245,11 +245,9 @@
                             isReversedLayout,
                             parentInfo.getDisplayId(),
                             isDraggableExpandType,
-                            getContainerBackgroundColor(
-                                    primaryContainer, DEFAULT_PRIMARY_VEIL_COLOR),
-                            getContainerBackgroundColor(
-                                    secondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR)
-                    ));
+                            primaryContainer,
+                            secondaryContainer)
+            );
         }
     }
 
@@ -965,8 +963,10 @@
         private final int mDisplayId;
         private final boolean mIsReversedLayout;
         private final boolean mIsDraggableExpandType;
-        private final Color mPrimaryVeilColor;
-        private final Color mSecondaryVeilColor;
+        @NonNull
+        private final TaskFragmentContainer mPrimaryContainer;
+        @NonNull
+        private final TaskFragmentContainer mSecondaryContainer;
         private final int mDividerWidthPx;
 
         @VisibleForTesting
@@ -979,8 +979,8 @@
                 boolean isReversedLayout,
                 int displayId,
                 boolean isDraggableExpandType,
-                @NonNull Color primaryVeilColor,
-                @NonNull Color secondaryVeilColor) {
+                @NonNull TaskFragmentContainer primaryContainer,
+                @NonNull TaskFragmentContainer secondaryContainer) {
             mConfiguration = configuration;
             mDividerAttributes = dividerAttributes;
             mDecorSurface = decorSurface;
@@ -989,8 +989,8 @@
             mIsReversedLayout = isReversedLayout;
             mDisplayId = displayId;
             mIsDraggableExpandType = isDraggableExpandType;
-            mPrimaryVeilColor = primaryVeilColor;
-            mSecondaryVeilColor = secondaryVeilColor;
+            mPrimaryContainer = primaryContainer;
+            mSecondaryContainer = secondaryContainer;
             mDividerWidthPx = getDividerWidthPx(dividerAttributes);
         }
 
@@ -1014,8 +1014,8 @@
                     && a.mDisplayId == b.mDisplayId
                     && a.mIsReversedLayout == b.mIsReversedLayout
                     && a.mIsDraggableExpandType == b.mIsDraggableExpandType
-                    && a.mPrimaryVeilColor.equals(b.mPrimaryVeilColor)
-                    && a.mSecondaryVeilColor.equals(b.mSecondaryVeilColor);
+                    && a.mPrimaryContainer == b.mPrimaryContainer
+                    && a.mSecondaryContainer == b.mSecondaryContainer;
         }
 
         private static boolean areSameSurfaces(
@@ -1328,8 +1328,12 @@
         }
 
         private void showVeils(@NonNull SurfaceControl.Transaction t) {
-            t.setColor(mPrimaryVeil, colorToFloatArray(mProperties.mPrimaryVeilColor))
-                    .setColor(mSecondaryVeil, colorToFloatArray(mProperties.mSecondaryVeilColor))
+            final Color primaryVeilColor = getContainerBackgroundColor(
+                    mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR);
+            final Color secondaryVeilColor = getContainerBackgroundColor(
+                    mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR);
+            t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor))
+                    .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor))
                     .setLayer(mDividerSurface, DIVIDER_LAYER)
                     .setLayer(mPrimaryVeil, VEIL_LAYER)
                     .setLayer(mSecondaryVeil, VEIL_LAYER)
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 4515187..3f67607 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -166,8 +166,8 @@
                 false /* isReversedLayout */,
                 Display.DEFAULT_DISPLAY,
                 false /* isDraggableExpandType */,
-                Color.valueOf(Color.BLACK), /* primaryVeilColor */
-                Color.valueOf(Color.GRAY) /* secondaryVeilColor */
+                mockPrimaryContainer,
+                mockSecondaryContainer
         );
 
         mDividerPresenter = new DividerPresenter(
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 08e695b..15f8c32 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -1,3 +1,5 @@
+# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto
+
 package: "com.android.wm.shell"
 container: "system"
 
@@ -99,3 +101,13 @@
     description: "Enable UI affordances to put other content into a bubble"
     bug: "342245211"
 }
+
+flag {
+    name: "only_reuse_bubbled_task_when_launched_from_bubble"
+    namespace: "multitasking"
+    description: "Allow reusing bubbled tasks for new activities only when launching from bubbles"
+    bug: "328229865"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
index 12d1927..ace2c13 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -26,7 +26,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.android.internal.protolog.common.ProtoLog
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.BubblePositioner
@@ -35,6 +35,8 @@
 import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
 import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
 import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -63,6 +65,9 @@
     private lateinit var controller: BubbleExpandedViewPinController
     private lateinit var testListener: TestLocationChangeListener
 
+    private val dropTargetView: View?
+        get() = container.findViewById(R.id.bubble_bar_drop_target)
+
     private val pointOnLeft = PointF(100f, 100f)
     private val pointOnRight = PointF(1900f, 500f)
 
@@ -92,13 +97,14 @@
 
     @After
     fun tearDown() {
-        runOnMainSync { controller.onDragEnd() }
+        getInstrumentation().runOnMainSync { controller.onDragEnd() }
         waitForAnimateOut()
     }
 
+    /** Dragging on same side should not show drop target or trigger location changes */
     @Test
-    fun drag_stayOnSameSide() {
-        runOnMainSync {
+    fun drag_stayOnRightSide() {
+        getInstrumentation().runOnMainSync {
             controller.onDragStart(initialLocationOnLeft = false)
             controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
             controller.onDragEnd()
@@ -106,71 +112,124 @@
         waitForAnimateIn()
         assertThat(dropTargetView).isNull()
         assertThat(testListener.locationChanges).isEmpty()
-        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
+        assertThat(testListener.locationReleases).containsExactly(RIGHT)
     }
 
+    /** Dragging on same side should not show drop target or trigger location changes */
     @Test
-    fun drag_toLeft() {
-        // Drag to left, but don't finish
-        runOnMainSync {
+    fun drag_stayOnLeftSide() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onDragEnd()
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).isEmpty()
+        assertThat(testListener.locationReleases).containsExactly(LEFT)
+    }
+
+    /** Drag crosses to the other side. Show drop target and trigger a location change. */
+    @Test
+    fun drag_rightToLeft() {
+        getInstrumentation().runOnMainSync {
             controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
             controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
         }
         waitForAnimateIn()
 
         assertThat(dropTargetView).isNotNull()
         assertThat(dropTargetView!!.alpha).isEqualTo(1f)
-
-        val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = true)
-        assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
-        assertThat(dropTargetView!!.layoutParams.height)
-            .isEqualTo(expectedDropTargetBounds.height())
-
-        assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+        assertThat(testListener.locationChanges).containsExactly(LEFT)
         assertThat(testListener.locationReleases).isEmpty()
-
-        // Finish the drag
-        runOnMainSync { controller.onDragEnd() }
-        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.LEFT)
     }
 
+    /** Drag crosses to the other side. Show drop target and trigger a location change. */
     @Test
-    fun drag_toLeftAndBackToRight() {
-        // Drag to left
-        runOnMainSync {
-            controller.onDragStart(initialLocationOnLeft = false)
+    fun drag_leftToRight() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
             controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
         }
         waitForAnimateIn()
+
+        assertThat(dropTargetView).isNotNull()
+        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+        assertThat(testListener.locationChanges).containsExactly(RIGHT)
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drop target does not initially show on the side that the drag starts. Check that it shows up
+     * after the dragging the view to other side and back to the initial side.
+     */
+    @Test
+    fun drag_rightToLeftToRight() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+
+        getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
+        waitForAnimateIn()
         assertThat(dropTargetView).isNotNull()
 
-        // Drag to right
-        runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) }
-        // We have to wait for existing drop target to animate out and new to animate in
+        getInstrumentation().runOnMainSync {
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
         waitForAnimateOut()
         waitForAnimateIn()
-
         assertThat(dropTargetView).isNotNull()
         assertThat(dropTargetView!!.alpha).isEqualTo(1f)
-
-        val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = false)
-        assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
-        assertThat(dropTargetView!!.layoutParams.height)
-            .isEqualTo(expectedDropTargetBounds.height())
-
-        assertThat(testListener.locationChanges)
-            .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+        assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
         assertThat(testListener.locationReleases).isEmpty()
-
-        // Release the view
-        runOnMainSync { controller.onDragEnd() }
-        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
     }
 
+    /**
+     * Drop target does not initially show on the side that the drag starts. Check that it shows up
+     * after the dragging the view to other side and back to the initial side.
+     */
     @Test
-    fun drag_toLeftInExclusionRect() {
-        runOnMainSync {
+    fun drag_leftToRightToLeft() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+
+        getInstrumentation().runOnMainSync {
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNotNull()
+
+        getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
+        waitForAnimateOut()
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNotNull()
+        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+        assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target
+     * should not show and location change should not trigger.
+     */
+    @Test
+    fun drag_rightToLeft_inExclusionRect() {
+        getInstrumentation().runOnMainSync {
             controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
             // Exclusion rect is around the bottom center area of the screen
             controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f)
         }
@@ -178,85 +237,212 @@
         assertThat(dropTargetView).isNull()
         assertThat(testListener.locationChanges).isEmpty()
         assertThat(testListener.locationReleases).isEmpty()
-
-        runOnMainSync { controller.onDragEnd() }
-        assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
     }
 
+    /**
+     * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target
+     * should not show and location change should not trigger.
+     */
     @Test
-    fun toggleSetDropTargetHidden_dropTargetExists() {
-        runOnMainSync {
+    fun drag_leftToRight_inExclusionRect() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            // Exclusion rect is around the bottom center area of the screen
+            controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).isEmpty()
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drag to dismiss target and back to the same side should not cause the drop target to show.
+     */
+    @Test
+    fun drag_rightToDismissToRight() {
+        getInstrumentation().runOnMainSync {
             controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+            controller.onStuckToDismissTarget()
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).isEmpty()
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drag to dismiss target and back to the same side should not cause the drop target to show.
+     */
+    @Test
+    fun drag_leftToDismissToLeft() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onStuckToDismissTarget()
             controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
         }
         waitForAnimateIn()
+        assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).isEmpty()
+        assertThat(testListener.locationReleases).isEmpty()
+    }
 
+    /** Drag to dismiss target and other side should show drop target on the other side. */
+    @Test
+    fun drag_rightToDismissToLeft() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+            controller.onStuckToDismissTarget()
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNotNull()
+        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+
+        assertThat(testListener.locationChanges).containsExactly(LEFT)
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /** Drag to dismiss target and other side should show drop target on the other side. */
+    @Test
+    fun drag_leftToDismissToRight() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onStuckToDismissTarget()
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNotNull()
+        assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+        assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+
+        assertThat(testListener.locationChanges).containsExactly(RIGHT)
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drag to dismiss should trigger a location change to the initial location, if the current
+     * location is different. And hide the drop target.
+     */
+    @Test
+    fun drag_rightToLeftToDismiss() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+        }
+        waitForAnimateIn()
         assertThat(dropTargetView).isNotNull()
         assertThat(dropTargetView!!.alpha).isEqualTo(1f)
 
-        runOnMainSync { controller.setDropTargetHidden(true) }
+        getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
         waitForAnimateOut()
-        assertThat(dropTargetView).isNotNull()
         assertThat(dropTargetView!!.alpha).isEqualTo(0f)
 
-        runOnMainSync { controller.setDropTargetHidden(false) }
+        assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
+        assertThat(testListener.locationReleases).isEmpty()
+    }
+
+    /**
+     * Drag to dismiss should trigger a location change to the initial location, if the current
+     * location is different. And hide the drop target.
+     */
+    @Test
+    fun drag_leftToRightToDismiss() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
         waitForAnimateIn()
         assertThat(dropTargetView).isNotNull()
         assertThat(dropTargetView!!.alpha).isEqualTo(1f)
-    }
-
-    @Test
-    fun toggleSetDropTargetHidden_noDropTarget() {
-        runOnMainSync { controller.setDropTargetHidden(true) }
+        getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
         waitForAnimateOut()
-        assertThat(dropTargetView).isNull()
-
-        runOnMainSync { controller.setDropTargetHidden(false) }
-        waitForAnimateIn()
-        assertThat(dropTargetView).isNull()
+        assertThat(dropTargetView!!.alpha).isEqualTo(0f)
+        assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
+        assertThat(testListener.locationReleases).isEmpty()
     }
 
+    /** Finishing drag should remove drop target and send location update. */
     @Test
-    fun onDragEnd_dropTargetExists() {
-        runOnMainSync {
+    fun drag_rightToLeftRelease() {
+        getInstrumentation().runOnMainSync {
             controller.onDragStart(initialLocationOnLeft = false)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
             controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
         }
         waitForAnimateIn()
         assertThat(dropTargetView).isNotNull()
 
-        runOnMainSync { controller.onDragEnd() }
+        getInstrumentation().runOnMainSync { controller.onDragEnd() }
         waitForAnimateOut()
         assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).containsExactly(LEFT)
+        assertThat(testListener.locationReleases).containsExactly(LEFT)
     }
 
+    /** Finishing drag should remove drop target and send location update. */
     @Test
-    fun onDragEnd_noDropTarget() {
-        runOnMainSync { controller.onDragEnd() }
+    fun drag_leftToRightRelease() {
+        getInstrumentation().runOnMainSync {
+            controller.onDragStart(initialLocationOnLeft = true)
+            controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+            controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+        }
+        waitForAnimateIn()
+        assertThat(dropTargetView).isNotNull()
+
+        getInstrumentation().runOnMainSync { controller.onDragEnd() }
         waitForAnimateOut()
         assertThat(dropTargetView).isNull()
+        assertThat(testListener.locationChanges).containsExactly(RIGHT)
+        assertThat(testListener.locationReleases).containsExactly(RIGHT)
     }
 
-    private val dropTargetView: View?
-        get() = container.findViewById(R.id.bubble_bar_drop_target)
-
-    private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect =
+    private fun getExpectedDropTargetBoundsOnLeft(): Rect =
         Rect().also {
-            positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it)
+            positioner.getBubbleBarExpandedViewBounds(
+                true /* onLeft */,
+                false /* isOverflowExpanded */,
+                it
+            )
         }
 
-    private fun runOnMainSync(runnable: Runnable) {
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
-    }
+    private fun getExpectedDropTargetBoundsOnRight(): Rect =
+        Rect().also {
+            positioner.getBubbleBarExpandedViewBounds(
+                false /* onLeft */,
+                false /* isOverflowExpanded */,
+                it
+            )
+        }
 
     private fun waitForAnimateIn() {
         // Advance animator for on-device test
-        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) }
+        getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION)
+        }
     }
 
     private fun waitForAnimateOut() {
         // Advance animator for on-device test
-        runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) }
+        getInstrumentation().runOnMainSync {
+            animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION)
+        }
+    }
+
+    private fun View.bounds(): Rect {
+        return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect ->
+            rect.offsetTo(x.toInt(), y.toInt())
+        }
     }
 
     internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index 6ca6517..dc022b4 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -69,7 +69,7 @@
 
     /** Returns {@code true} if the transition is opening or closing mode. */
     public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
-        return isOpeningMode(mode) || mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+        return isOpeningMode(mode) || isClosingMode(mode);
     }
 
     /** Returns {@code true} if the transition is opening mode. */
@@ -77,6 +77,11 @@
         return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
     }
 
+    /** Returns {@code true} if the transition is closing mode. */
+    public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) {
+        return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+    }
+
     /** Returns {@code true} if the transition has a display change. */
     public static boolean hasDisplayChange(@NonNull TransitionInfo info) {
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
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 ee740fb..9114c7a 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
@@ -27,6 +27,7 @@
 import android.graphics.Rect
 import android.graphics.RectF
 import android.os.RemoteException
+import android.util.TimeUtils
 import android.view.Choreographer
 import android.view.Display
 import android.view.IRemoteAnimationFinishedCallback
@@ -109,6 +110,7 @@
     private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
             .setStiffness(SpringForce.STIFFNESS_LOW)
             .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+    protected var gestureProgress = 0f
 
     /** Background color to be used during the animation, also see [getBackgroundColor] */
     protected var customizedBackgroundColor = 0
@@ -212,6 +214,7 @@
 
     private fun onGestureProgress(backEvent: BackEvent) {
         val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+        gestureProgress = progress
         currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
         val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
         currentClosingRect.offset(0f, yOffset)
@@ -257,12 +260,16 @@
         }
 
         // kick off spring animation with the current velocity from the pre-commit phase, this
-        // affects the scaling of the closing activity during post-commit
+        // affects the scaling of the closing and/or opening activity during post-commit
+        val startVelocity =
+            if (gestureProgress < 0.1f) -DEFAULT_FLING_VELOCITY else -velocity * SPRING_SCALE
         val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
-            .setStartVelocity(min(0f, -velocity * SPRING_SCALE))
+            .setStartVelocity(startVelocity.coerceIn(-MAX_FLING_VELOCITY, 0f))
             .setStartValue(SPRING_SCALE)
             .setSpring(postCommitFlingSpring)
         flingAnimation.start()
+        // do an animation-frame immediately to prevent idle frame
+        flingAnimation.doAnimationFrame(choreographer.lastFrameTimeNanos / TimeUtils.NANOS_PER_MS)
 
         val valueAnimator =
             ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
@@ -292,6 +299,7 @@
         enteringTarget?.let {
             if (it.leash != null && it.leash.isValid) {
                 transaction.setCornerRadius(it.leash, 0f)
+                if (!triggerBack) transaction.setAlpha(it.leash, 0f)
                 it.leash.release()
             }
             enteringTarget = null
@@ -315,19 +323,22 @@
         isLetterboxed = false
         enteringHasSameLetterbox = false
         lastPostCommitFlingScale = SPRING_SCALE
+        gestureProgress = 0f
     }
 
     protected fun applyTransform(
         leash: SurfaceControl?,
         rect: RectF,
         alpha: Float,
-        baseTransformation: Transformation? = null
+        baseTransformation: Transformation? = null,
+        flingMode: FlingMode = FlingMode.NO_FLING
     ) {
         if (leash == null || !leash.isValid) return
         tempRectF.set(rect)
-        if (leash == closingTarget?.leash) {
-            lastPostCommitFlingScale = (postCommitFlingScale.value / SPRING_SCALE).coerceIn(
-                    minimumValue = MAX_FLING_SCALE, maximumValue = lastPostCommitFlingScale
+        if (flingMode != FlingMode.NO_FLING) {
+            lastPostCommitFlingScale = min(
+                postCommitFlingScale.value / SPRING_SCALE,
+                if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
             )
             // apply an additional scale to the closing target to account for fling velocity
             tempRectF.scaleCentered(lastPostCommitFlingScale)
@@ -529,14 +540,30 @@
         private const val MAX_SCRIM_ALPHA_DARK = 0.8f
         private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
         private const val SPRING_SCALE = 100f
-        private const val MAX_FLING_SCALE = 0.6f
+        private const val MAX_FLING_VELOCITY = 1000f
+        private const val DEFAULT_FLING_VELOCITY = 120f
+    }
+
+    enum class FlingMode {
+        NO_FLING,
+
+        /**
+         * This is used for the closing target in custom cross-activity back animations. When the
+         * back gesture is flung, the closing target shrinks a bit further with a spring motion.
+         */
+        FLING_SHRINK,
+
+        /**
+         * This is used for the closing and opening target in the default cross-activity back
+         * animation. When the back gesture is flung, the closing and opening targets shrink a
+         * bit further and then bounce back with a spring motion.
+         */
+        FLING_BOUNCE
     }
 }
 
 // The target will loose focus when alpha == 0, so keep a minimum value for it.
-private fun keepMinimumAlpha(transAlpha: Float): Float {
-    return max(transAlpha.toDouble(), 0.005).toFloat()
-}
+private fun keepMinimumAlpha(transAlpha: Float) = max(transAlpha, 0.005f)
 
 private fun isDarkMode(context: Context): Boolean {
     return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
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 c4aafea..c738ce5 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
@@ -57,7 +57,6 @@
     private var enterAnimation: Animation? = null
     private var closeAnimation: Animation? = null
     private val transformation = Transformation()
-    private var gestureProgress = 0f
 
     override val allowEnteringYShift = false
 
@@ -105,7 +104,6 @@
     }
 
     override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation {
-        gestureProgress = progress
         transformation.clear()
         enterAnimation!!.getTransformationAt(progress * PRE_COMMIT_MAX_PROGRESS, transformation)
         return transformation
@@ -134,7 +132,13 @@
         if (closingTarget == null || enteringTarget == null) return
 
         val closingProgress = closeAnimation!!.getPostCommitProgress(linearProgress)
-        applyTransform(closingTarget!!.leash, currentClosingRect, closingProgress, closeAnimation!!)
+        applyTransform(
+            closingTarget!!.leash,
+            currentClosingRect,
+            closingProgress,
+            closeAnimation!!,
+            FlingMode.FLING_SHRINK
+        )
         val enteringProgress = MathUtils.lerp(
             gestureProgress * PRE_COMMIT_MAX_PROGRESS,
             1f,
@@ -144,7 +148,8 @@
             enteringTarget!!.leash,
             currentEnteringRect,
             enteringProgress,
-            enterAnimation!!
+            enterAnimation!!,
+            FlingMode.NO_FLING
         )
         applyTransaction()
     }
@@ -153,11 +158,12 @@
         leash: SurfaceControl,
         rect: RectF,
         progress: Float,
-        animation: Animation
+        animation: Animation,
+        flingMode: FlingMode
     ) {
         transformation.clear()
         animation.getTransformationAt(progress, transformation)
-        applyTransform(leash, rect, transformation.alpha, transformation)
+        applyTransform(leash, rect, transformation.alpha, transformation, flingMode)
     }
 
     override fun finishAnimation() {
@@ -166,7 +172,6 @@
         enterAnimation?.reset()
         enterAnimation = null
         transformation.clear()
-        gestureProgress = 0f
         super.finishAnimation()
     }
 
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 44752fe..3b5eb36 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
@@ -43,7 +43,7 @@
         Choreographer.getInstance()
     ) {
 
-    private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN
+    private val postCommitInterpolator = Interpolators.EMPHASIZED
     private val enteringStartOffset =
         context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
     override val allowEnteringYShift = true
@@ -87,17 +87,27 @@
 
     override fun onPostCommitProgress(linearProgress: Float) {
         super.onPostCommitProgress(linearProgress)
-        val closingAlpha = max(1f - linearProgress * 2, 0f)
+        val closingAlpha = max(1f - linearProgress * 5, 0f)
         val progress = postCommitInterpolator.getInterpolation(linearProgress)
         currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
-        applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+        applyTransform(
+            closingTarget?.leash,
+            currentClosingRect,
+            closingAlpha,
+            flingMode = FlingMode.FLING_BOUNCE
+        )
         currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
-        applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+        applyTransform(
+            enteringTarget?.leash,
+            currentEnteringRect,
+            1f,
+            flingMode = FlingMode.FLING_BOUNCE
+        )
         applyTransaction()
     }
 
 
     companion object {
-        private const val POST_COMMIT_DURATION = 300L
+        private const val POST_COMMIT_DURATION = 450L
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
index a51ac63..fa1091c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
@@ -150,7 +150,7 @@
             draggedObject: MagnetizedObject<*>
         ) {
             isStuckToDismiss = true
-            pinController.setDropTargetHidden(true)
+            pinController.onStuckToDismissTarget()
         }
 
         override fun onUnstuckFromTarget(
@@ -162,7 +162,6 @@
         ) {
             isStuckToDismiss = false
             animationHelper.animateUnstuckFromDismissView(target)
-            pinController.setDropTargetHidden(false)
         }
 
         override fun onReleasedInTarget(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
index e514f9d..eec2468 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
@@ -38,8 +38,10 @@
  */
 abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) {
 
+    private var initialLocationOnLeft = false
     private var onLeft = false
     private var dismissZone: RectF? = null
+    private var stuckToDismissTarget = false
     private var screenCenterX = 0
     private var listener: LocationChangeListener? = null
     private var dropTargetAnimator: ObjectAnimator? = null
@@ -50,6 +52,7 @@
      * @param initialLocationOnLeft side of the screen where bubble bar is pinned to
      */
     fun onDragStart(initialLocationOnLeft: Boolean) {
+        this.initialLocationOnLeft = initialLocationOnLeft
         onLeft = initialLocationOnLeft
         screenCenterX = screenSizeProvider.invoke().x / 2
         dismissZone = getExclusionRect()
@@ -59,22 +62,33 @@
     fun onDragUpdate(x: Float, y: Float) {
         if (dismissZone?.contains(x, y) == true) return
 
-        if (onLeft && x > screenCenterX) {
-            onLeft = false
-            onLocationChange(RIGHT)
-        } else if (!onLeft && x < screenCenterX) {
-            onLeft = true
-            onLocationChange(LEFT)
+        val wasOnLeft = onLeft
+        onLeft = x < screenCenterX
+        if (wasOnLeft != onLeft) {
+            onLocationChange(if (onLeft) LEFT else RIGHT)
+        } else if (stuckToDismissTarget) {
+            // Moved out of the dismiss view back to initial side, if we have a drop target, show it
+            getDropTargetView()?.apply { animateIn() }
         }
+        // Make sure this gets cleared
+        stuckToDismissTarget = false
     }
 
-    /** Temporarily hide the drop target view */
-    fun setDropTargetHidden(hidden: Boolean) {
-        val targetView = getDropTargetView() ?: return
-        if (hidden) {
-            targetView.animateOut()
-        } else {
-            targetView.animateIn()
+    /** Signal the controller that view has been dragged to dismiss view. */
+    fun onStuckToDismissTarget() {
+        stuckToDismissTarget = true
+        // Notify that location may be reset
+        val shouldResetLocation = onLeft != initialLocationOnLeft
+        if (shouldResetLocation) {
+            onLeft = initialLocationOnLeft
+            listener?.onChange(if (onLeft) LEFT else RIGHT)
+        }
+        getDropTargetView()?.apply {
+            animateOut {
+                if (shouldResetLocation) {
+                    updateLocation(if (onLeft) LEFT else RIGHT)
+                }
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index c79eef7e..cd478e5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -143,6 +143,10 @@
     }
 
     public static boolean handles(TransitionInfo info) {
+        // There is no animation for screen-wake unless we are immediately unlocking.
+        if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) {
+            return false;
+        }
         return (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 6224543..6ade81c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -1591,7 +1591,7 @@
         public void setHomeTransitionListener(IHomeTransitionListener listener) {
             executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener",
                     (transitions) -> {
-                        transitions.mHomeTransitionObserver.setHomeTransitionListener(mTransitions,
+                        transitions.mHomeTransitionObserver.setHomeTransitionListener(transitions,
                                 listener);
                     });
         }
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 6901e75..37cdbb4 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
@@ -583,17 +583,20 @@
             } else if (ev.getAction() == ACTION_HOVER_MOVE
                     && MaximizeMenu.Companion.isMaximizeMenuView(id)) {
                 decoration.onMaximizeMenuHoverMove(id, ev);
+                mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
             } else if (ev.getAction() == ACTION_HOVER_EXIT) {
                 if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
                     decoration.onMaximizeWindowHoverExit();
-                } else if (id == R.id.maximize_window || id == R.id.maximize_menu) {
+                } else if (id == R.id.maximize_window
+                        || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
                     // Close menu if not hovering over maximize menu or maximize button after a
                     // delay to give user a chance to re-enter view or to move from one maximize
                     // menu view to another.
                     mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
                             CLOSE_MAXIMIZE_MENU_DELAY_MS);
-                } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) {
-                    decoration.onMaximizeMenuHoverExit(id, ev);
+                    if (id != R.id.maximize_window) {
+                        decoration.onMaximizeMenuHoverExit(id, ev);
+                    }
                 }
                 return true;
             }
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 78f0ef7..4f04901 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
@@ -88,7 +88,7 @@
     }
 
     fun cancelHoverAnimation() {
-        hoverProgressAnimatorSet.removeAllListeners()
+        hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() }
         hoverProgressAnimatorSet.cancel()
         progressBar.visibility = View.INVISIBLE
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index ec20471..7ade987 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
@@ -23,13 +23,13 @@
 
 val TaskInfo.isTransparentCaptionBarAppearance: Boolean
     get() {
-        val appearance = taskDescription?.systemBarsAppearance ?: 0
+        val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0
         return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0
     }
 
 val TaskInfo.isLightCaptionBarAppearance: Boolean
     get() {
-        val appearance = taskDescription?.systemBarsAppearance ?: 0
+        val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0
         return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
index 285e5b6..51b291c0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
@@ -39,7 +39,7 @@
 class DesktopModeUiEventLoggerTest : ShellTestCase() {
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
     private lateinit var logger: DesktopModeUiEventLogger
-    private val instanceIdSequence = InstanceIdSequence(10)
+    private val instanceIdSequence = InstanceIdSequence(/* instanceIdMax */ 1 shl 20)
 
 
     @Before
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 3ca9b57..a731e53 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -228,7 +228,7 @@
     public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        taskInfo.taskDescription.setSystemBarsAppearance(
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(
                 APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND);
         final RelayoutParams relayoutParams = new RelayoutParams();
 
@@ -246,7 +246,7 @@
     public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() {
         final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
         taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
-        taskInfo.taskDescription.setSystemBarsAppearance(0);
+        taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0);
         final RelayoutParams relayoutParams = new RelayoutParams();
 
         DesktopModeWindowDecoration.updateRelayoutParams(
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 5ce990f..7b7ccf5 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -48,7 +48,6 @@
         "libgui",
         "libui",
         "libinput",
-        "libnativewindow",
     ],
 
     header_libs: [
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index f1ee325..eecc741 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -165,6 +165,15 @@
     }
 }
 
+void MouseCursorController::setSkipScreenshot(bool skip) {
+    std::scoped_lock lock(mLock);
+    if (mLocked.skipScreenshot == skip) {
+        return;
+    }
+    mLocked.skipScreenshot = skip;
+    updatePointerLocked();
+}
+
 void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) {
     std::scoped_lock lock(mLock);
 
@@ -352,6 +361,7 @@
     mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
     mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
     mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId);
+    mLocked.pointerSprite->setSkipScreenshot(mLocked.skipScreenshot);
 
     if (mLocked.pointerAlpha > 0) {
         mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index dc7e8ca..78f6413 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -53,6 +53,9 @@
     void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources);
     void setStylusHoverMode(bool stylusHoverMode);
 
+    // Set/Unset flag to hide the mouse cursor on the mirrored display
+    void setSkipScreenshot(bool skip);
+
     void updatePointerIcon(PointerIconStyle iconId);
     void setCustomPointerIcon(const SpriteIcon& icon);
     void reloadPointerResources(bool getAdditionalMouseResources);
@@ -94,6 +97,7 @@
         PointerIconStyle requestedPointerType;
         PointerIconStyle resolvedPointerType;
 
+        bool skipScreenshot{false};
         bool animating{false};
 
     } mLocked GUARDED_BY(mLock);
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index cca1b07..11b27a2 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -286,13 +286,16 @@
     mCursorController.setCustomPointerIcon(icon);
 }
 
-void PointerController::setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) {
+void PointerController::setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) {
     std::scoped_lock lock(getLock());
-    if (skip) {
-        mLocked.displaysToSkipScreenshot.insert(displayId);
-    } else {
-        mLocked.displaysToSkipScreenshot.erase(displayId);
-    }
+    mLocked.displaysToSkipScreenshot.insert(displayId);
+    mCursorController.setSkipScreenshot(true);
+}
+
+void PointerController::clearSkipScreenshotFlags() {
+    std::scoped_lock lock(getLock());
+    mLocked.displaysToSkipScreenshot.clear();
+    mCursorController.setSkipScreenshot(false);
 }
 
 void PointerController::doInactivityTimeout() {
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index c6430f7..4d1e1d7 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -66,7 +66,8 @@
     void clearSpots() override;
     void updatePointerIcon(PointerIconStyle iconId) override;
     void setCustomPointerIcon(const SpriteIcon& icon) override;
-    void setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) override;
+    void setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override;
+    void clearSkipScreenshotFlags() override;
 
     virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
     void doInactivityTimeout();
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index 2dcb1f1..5b00fca 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -162,6 +162,16 @@
 };
 
 class PointerControllerTest : public Test {
+private:
+    void loopThread();
+
+    std::atomic<bool> mRunning = true;
+    class MyLooper : public Looper {
+    public:
+        MyLooper() : Looper(false) {}
+        ~MyLooper() = default;
+    };
+
 protected:
     PointerControllerTest();
     ~PointerControllerTest();
@@ -173,22 +183,16 @@
     std::unique_ptr<MockSpriteController> mSpriteController;
     std::shared_ptr<PointerController> mPointerController;
     sp<android::gui::WindowInfosListener> mRegisteredListener;
+    sp<MyLooper> mLooper;
 
 private:
-    void loopThread();
-
-    std::atomic<bool> mRunning = true;
-    class MyLooper : public Looper {
-    public:
-        MyLooper() : Looper(false) {}
-        ~MyLooper() = default;
-    };
-    sp<MyLooper> mLooper;
     std::thread mThread;
 };
 
-PointerControllerTest::PointerControllerTest() : mPointerSprite(new NiceMock<MockSprite>),
-        mLooper(new MyLooper), mThread(&PointerControllerTest::loopThread, this) {
+PointerControllerTest::PointerControllerTest()
+      : mPointerSprite(new NiceMock<MockSprite>),
+        mLooper(new MyLooper),
+        mThread(&PointerControllerTest::loopThread, this) {
     mSpriteController.reset(new NiceMock<MockSpriteController>(mLooper));
     mPolicy = new MockPointerControllerPolicyInterface();
 
@@ -339,7 +343,7 @@
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 
     // Marking the display to skip screenshot should update sprite as well
-    mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, true);
+    mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT);
     EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true));
 
     // Update spots to sync state with sprite
@@ -348,13 +352,53 @@
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 
     // Reset flag and verify again
-    mPointerController->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, false);
+    mPointerController->clearSkipScreenshotFlags();
     EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
     mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
                                  ui::LogicalDisplayId::DEFAULT);
     testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
 }
 
+class PointerControllerSkipScreenshotFlagTest
+      : public PointerControllerTest,
+        public testing::WithParamInterface<PointerControllerInterface::ControllerType> {};
+
+TEST_P(PointerControllerSkipScreenshotFlagTest, updatesSkipScreenshotFlag) {
+    sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>);
+    EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite));
+
+    // Create a pointer controller
+    mPointerController =
+            PointerController::create(mPolicy, mLooper, *mSpriteController, GetParam());
+    ensureDisplayViewportIsSet(ui::LogicalDisplayId::DEFAULT);
+
+    // By default skip screenshot flag is not set for the sprite
+    EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false));
+
+    // Update pointer to sync state with sprite
+    mPointerController->setPosition(100, 100);
+    testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
+
+    // Marking the controller to skip screenshot should update pointer sprite
+    mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT);
+    EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(true));
+
+    // Update pointer to sync state with sprite
+    mPointerController->move(10, 10);
+    testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
+
+    // Reset flag and verify again
+    mPointerController->clearSkipScreenshotFlags();
+    EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false));
+    mPointerController->move(10, 10);
+    testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
+}
+
+INSTANTIATE_TEST_SUITE_P(PointerControllerSkipScreenshotFlagTest,
+                         PointerControllerSkipScreenshotFlagTest,
+                         testing::Values(PointerControllerInterface::ControllerType::MOUSE,
+                                         PointerControllerInterface::ControllerType::STYLUS));
+
 class PointerControllerWindowInfoListenerTest : public Test {};
 
 TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 293c561..d148afd 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -1764,6 +1764,10 @@
     public static native int getForceUse(int usage);
     /** @hide */
     @UnsupportedAppUsage
+    public static native int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType,
+            @NonNull String address, boolean enabled, int streamToDriveAbs);
+    /** @hide */
+    @UnsupportedAppUsage
     public static native int initStreamVolume(int stream, int indexMin, int indexMax);
     @UnsupportedAppUsage
     private static native int setStreamVolumeIndex(int stream, int index, int device);
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java
index 70462ef..442ccdc 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/MediaController.java
@@ -141,10 +141,9 @@
         }
         try {
             return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent);
-        } catch (RemoteException e) {
-            // System is dead. =(
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return false;
     }
 
     /**
@@ -155,9 +154,8 @@
     public @Nullable PlaybackState getPlaybackState() {
         try {
             return mSessionBinder.getPlaybackState();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getPlaybackState.", e);
-            return null;
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -169,9 +167,8 @@
     public @Nullable MediaMetadata getMetadata() {
         try {
             return mSessionBinder.getMetadata();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getMetadata.", e);
-            return null;
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -185,10 +182,9 @@
         try {
             ParceledListSlice list = mSessionBinder.getQueue();
             return list == null ? null : list.getList();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getQueue.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -197,10 +193,9 @@
     public @Nullable CharSequence getQueueTitle() {
         try {
             return mSessionBinder.getQueueTitle();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getQueueTitle", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -209,10 +204,9 @@
     public @Nullable Bundle getExtras() {
         try {
             return mSessionBinder.getExtras();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getExtras", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -232,9 +226,8 @@
     public int getRatingType() {
         try {
             return mSessionBinder.getRatingType();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getRatingType.", e);
-            return Rating.RATING_NONE;
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -246,10 +239,9 @@
     public long getFlags() {
         try {
             return mSessionBinder.getFlags();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getFlags.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return 0;
     }
 
     /** Returns the current playback info for this session. */
@@ -271,10 +263,9 @@
     public @Nullable PendingIntent getSessionActivity() {
         try {
             return mSessionBinder.getLaunchPendingIntent();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling getPendingIntent.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -304,8 +295,8 @@
             //       AppOpsManager usages.
             mSessionBinder.setVolumeTo(mContext.getPackageName(), mContext.getOpPackageName(),
                     value, flags);
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling setVolumeTo.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -329,8 +320,8 @@
             //       AppOpsManager usages.
             mSessionBinder.adjustVolume(mContext.getPackageName(), mContext.getOpPackageName(),
                     direction, flags);
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -395,8 +386,8 @@
         }
         try {
             mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb);
-        } catch (RemoteException e) {
-            Log.d(TAG, "Dead object in sendCommand.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -409,8 +400,8 @@
         if (mPackageName == null) {
             try {
                 mPackageName = mSessionBinder.getPackageName();
-            } catch (RemoteException e) {
-                Log.d(TAG, "Dead object in getPackageName.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
         return mPackageName;
@@ -430,8 +421,8 @@
         // Get info from the connected session.
         try {
             mSessionInfo = mSessionBinder.getSessionInfo();
-        } catch (RemoteException e) {
-            Log.d(TAG, "Dead object in getSessionInfo.", e);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
 
         if (mSessionInfo == null) {
@@ -454,8 +445,8 @@
         if (mTag == null) {
             try {
                 mTag = mSessionBinder.getTag();
-            } catch (RemoteException e) {
-                Log.d(TAG, "Dead object in getTag.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
         return mTag;
@@ -485,8 +476,8 @@
             try {
                 mSessionBinder.registerCallback(mContext.getPackageName(), mCbStub);
                 mCbRegistered = true;
-            } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in registerCallback", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
@@ -504,8 +495,8 @@
         if (mCbRegistered && mCallbacks.size() == 0) {
             try {
                 mSessionBinder.unregisterCallback(mCbStub);
-            } catch (RemoteException e) {
-                Log.e(TAG, "Dead object in removeCallbackLocked");
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
             mCbRegistered = false;
         }
@@ -641,8 +632,8 @@
         public void prepare() {
             try {
                 mSessionBinder.prepare(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling prepare.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -665,8 +656,8 @@
             }
             try {
                 mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -691,8 +682,8 @@
             }
             try {
                 mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling prepare(" + query + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -715,8 +706,8 @@
             }
             try {
                 mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling prepare(" + uri + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -726,8 +717,8 @@
         public void play() {
             try {
                 mSessionBinder.play(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling play.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -745,8 +736,8 @@
             }
             try {
                 mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -767,8 +758,8 @@
             }
             try {
                 mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling play(" + query + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -786,8 +777,8 @@
             }
             try {
                 mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling play(" + uri + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -798,8 +789,8 @@
         public void skipToQueueItem(long id) {
             try {
                 mSessionBinder.skipToQueueItem(mContext.getPackageName(), id);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -810,8 +801,8 @@
         public void pause() {
             try {
                 mSessionBinder.pause(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling pause.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -822,8 +813,8 @@
         public void stop() {
             try {
                 mSessionBinder.stop(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling stop.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -835,8 +826,8 @@
         public void seekTo(long pos) {
             try {
                 mSessionBinder.seekTo(mContext.getPackageName(), pos);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling seekTo.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -847,8 +838,8 @@
         public void fastForward() {
             try {
                 mSessionBinder.fastForward(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling fastForward.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -858,8 +849,8 @@
         public void skipToNext() {
             try {
                 mSessionBinder.next(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling next.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -870,8 +861,8 @@
         public void rewind() {
             try {
                 mSessionBinder.rewind(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling rewind.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -881,8 +872,8 @@
         public void skipToPrevious() {
             try {
                 mSessionBinder.previous(mContext.getPackageName());
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling previous.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -896,8 +887,8 @@
         public void setRating(Rating rating) {
             try {
                 mSessionBinder.rate(mContext.getPackageName(), rating);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling rate.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -914,8 +905,8 @@
             }
             try {
                 mSessionBinder.setPlaybackSpeed(mContext.getPackageName(), speed);
-            } catch (RemoteException e) {
-                Log.wtf(TAG, "Error calling setPlaybackSpeed.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
 
@@ -949,8 +940,8 @@
             }
             try {
                 mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args);
-            } catch (RemoteException e) {
-                Log.d(TAG, "Dead object in sendCustomAction.", e);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index a33e225..055ccbc 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -27,6 +27,7 @@
     field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC";
     field @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.SHOW_CUSTOMIZED_RESOLVER) public static final String ACTION_SHOW_NFC_RESOLVER = "android.nfc.action.SHOW_NFC_RESOLVER";
     field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String EXTRA_RESOLVE_INFOS = "android.nfc.extra.RESOLVE_INFOS";
+    field @FlaggedApi("android.nfc.nfc_set_default_disc_tech") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final int FLAG_SET_DEFAULT_TECH = 1073741824; // 0x40000000
     field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int MESSAGE_TYPE_COMMAND = 1; // 0x1
     field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_FAILED = 3; // 0x3
     field @FlaggedApi("android.nfc.nfc_vendor_cmd") public static final int SEND_VENDOR_NCI_STATUS_MESSAGE_CORRUPTED = 2; // 0x2
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 698df28..1dfc81e 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -340,7 +340,8 @@
     public static final int FLAG_READER_NFC_BARCODE = 0x10;
 
     /** @hide */
-    @IntDef(flag = true, prefix = {"FLAG_READER_"}, value = {
+    @IntDef(flag = true, value = {
+        FLAG_SET_DEFAULT_TECH,
         FLAG_READER_KEEP,
         FLAG_READER_DISABLE,
         FLAG_READER_NFC_A,
@@ -438,7 +439,8 @@
     public static final int FLAG_USE_ALL_TECH = 0xff;
 
     /** @hide */
-    @IntDef(flag = true, prefix = {"FLAG_LISTEN_"}, value = {
+    @IntDef(flag = true, value = {
+        FLAG_SET_DEFAULT_TECH,
         FLAG_LISTEN_KEEP,
         FLAG_LISTEN_DISABLE,
         FLAG_LISTEN_NFC_PASSIVE_A,
@@ -449,6 +451,18 @@
     public @interface ListenTechnology {}
 
     /**
+     * Flag used in {@link #setDiscoveryTechnology(Activity, int, int)}.
+     * <p>
+     * Setting this flag changes the default listen or poll tech.
+     * Only available to privileged apps.
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_NFC_SET_DEFAULT_DISC_TECH)
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    public static final int FLAG_SET_DEFAULT_TECH = 0x40000000;
+
+    /**
      * @hide
      * @removed
      */
@@ -1874,14 +1888,6 @@
     public void setDiscoveryTechnology(@NonNull Activity activity,
             @PollTechnology int pollTechnology, @ListenTechnology int listenTechnology) {
 
-        // A special treatment of the _KEEP flags
-        if ((listenTechnology & FLAG_LISTEN_KEEP) != 0) {
-            listenTechnology = -1;
-        }
-        if ((pollTechnology & FLAG_READER_KEEP) != 0) {
-            pollTechnology = -1;
-        }
-
         if (listenTechnology == FLAG_LISTEN_DISABLE) {
             synchronized (sLock) {
                 if (!sHasNfcFeature) {
@@ -1901,7 +1907,25 @@
                 }
             }
         }
-        mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology);
+    /*
+     * Privileged FLAG to set technology mask for all data processed by NFC controller
+     * Note: Use with caution! The app is responsible for ensuring that the discovery
+     * technology mask is returned to default.
+     * Note: FLAG_USE_ALL_TECH used with _KEEP flags will reset the technolody to android default
+     */
+        if (Flags.nfcSetDefaultDiscTech()
+                && ((pollTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH
+                || (listenTechnology & FLAG_SET_DEFAULT_TECH) == FLAG_SET_DEFAULT_TECH)) {
+            Binder token = new Binder();
+            try {
+                NfcAdapter.sService.updateDiscoveryTechnology(token,
+                        pollTechnology, listenTechnology);
+            } catch (RemoteException e) {
+                attemptDeadServiceRecovery(e);
+            }
+        } else {
+            mNfcActivityManager.setDiscoveryTech(activity, pollTechnology, listenTechnology);
+        }
     }
 
     /**
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index 4c76fb0..5dcc84c 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -16,7 +16,6 @@
 
 package android.nfc.cardemulation;
 
-import android.annotation.DurationMillisLong;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -33,13 +32,13 @@
 
 /**
  * Polling Frames represent data about individual frames of an NFC polling loop. These frames will
- * be deliverd to subclasses of {@link HostApduService} that have registered filters with
- * {@link CardEmulation#registerPollingLoopFilterForService(ComponentName, String)} that match a
- * given frame in a loop and will be delivered through calls to
+ * be delivered to subclasses of {@link HostApduService} that have registered filters with
+ * {@link CardEmulation#registerPollingLoopFilterForService(ComponentName, String, boolean)} that
+ * match a given frame in a loop and will be delivered through calls to
  * {@link HostApduService#processPollingFrames(List)}.
  */
 @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP)
-public final class PollingFrame implements Parcelable{
+public final class PollingFrame implements Parcelable {
 
     /**
      * @hide
@@ -146,7 +145,6 @@
     private final int mType;
     private final byte[] mData;
     private final int mGain;
-    @DurationMillisLong
     private final long mTimestamp;
     private boolean mTriggeredAutoTransact;
 
@@ -179,18 +177,18 @@
      * @param type the type of the frame
      * @param data a byte array of the data contained in the frame
      * @param gain the vendor-specific gain of the field
-     * @param timestampMillis the timestamp in millisecones
+     * @param timestampMicros the timestamp in microseconds
      * @param triggeredAutoTransact whether or not this frame triggered the device to start a
      * transaction automatically
      *
      * @hide
      */
     public PollingFrame(@PollingFrameType int type, @Nullable byte[] data,
-            int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) {
+            int gain, long timestampMicros, boolean triggeredAutoTransact) {
         mType = type;
         mData = data == null ? new byte[0] : data;
         mGain = gain;
-        mTimestamp = timestampMillis;
+        mTimestamp = timestampMicros;
         mTriggeredAutoTransact = triggeredAutoTransact;
     }
 
@@ -198,11 +196,11 @@
      * Returns the type of frame for this polling loop frame.
      * The possible return values are:
      * <ul>
-     *   <li>{@link POLLING_LOOP_TYPE_ON}</li>
-     *   <li>{@link POLLING_LOOP_TYPE_OFF}</li>
-     *   <li>{@link POLLING_LOOP_TYPE_A}</li>
-     *   <li>{@link POLLING_LOOP_TYPE_B}</li>
-     *   <li>{@link POLLING_LOOP_TYPE_F}</li>
+     *   <li>{@link #POLLING_LOOP_TYPE_ON}</li>
+     *   <li>{@link #POLLING_LOOP_TYPE_OFF}</li>
+     *   <li>{@link #POLLING_LOOP_TYPE_A}</li>
+     *   <li>{@link #POLLING_LOOP_TYPE_B}</li>
+     *   <li>{@link #POLLING_LOOP_TYPE_F}</li>
      * </ul>
      */
     public @PollingFrameType int getType() {
@@ -226,12 +224,12 @@
     }
 
     /**
-     * Returns the timestamp of when the polling loop frame was observed in milliseconds. These
-     * timestamps are relative and not absolute and should only be used for comparing the timing of
-     * frames relative to each other.
-     * @return the timestamp in milliseconds
+     * Returns the timestamp of when the polling loop frame was observed, in microseconds. These
+     * timestamps are relative and should only be used for comparing the timing of frames relative
+     * to each other.
+     * @return the timestamp in microseconds
      */
-    public @DurationMillisLong long getTimestamp() {
+    public long getTimestamp() {
         return mTimestamp;
     }
 
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index cb2a48c..b242a76 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -101,3 +101,12 @@
     description: "Enable nfc state change API"
     bug: "319934052"
 }
+
+flag {
+    name: "nfc_set_default_disc_tech"
+    is_exported: true
+    namespace: "nfc"
+    description: "Flag for NFC set default disc tech API"
+    bug: "321311407"
+}
+
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
index 9c8ec3b..3022fa0 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -27,6 +27,7 @@
 import android.credentials.selection.Entry
 import android.credentials.selection.GetCredentialProviderData
 import android.graphics.drawable.Drawable
+import android.os.Bundle
 import android.text.TextUtils
 import android.util.Log
 import androidx.activity.result.IntentSenderRequest
@@ -227,26 +228,31 @@
  * and get flows utilize slice params; includes the final '.' before the name of the type (e.g.
  * androidx.credentials.provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS must have
  * 'hintPrefix' up to "androidx.credentials.provider.credentialEntry.")
- * // TODO(b/326243754) : Presently, due to dependencies, the opId bit is parsed but is never
- * // expected to be used. When it is added, it should be lightly validated.
  */
 fun retrieveEntryBiometricRequest(
     entry: Entry,
-    hintPrefix: String,
+    hintPrefix: String
 ): BiometricRequestInfo? {
-    // TODO(b/326243754) : When available, use the official jetpack structured type
-    val allowedAuthenticators: Int? = entry.slice.items.firstOrNull {
-        it.hasHint(hintPrefix + "SLICE_HINT_ALLOWED_AUTHENTICATORS")
-    }?.int
+    // TODO(b/326243754) : When available, use the official jetpack structured typLo
+    val biometricPromptDataBundleKey = "SLICE_HINT_BIOMETRIC_PROMPT_DATA"
+    val biometricPromptDataBundle: Bundle = entry.slice.items.firstOrNull {
+        it.hasHint(hintPrefix + biometricPromptDataBundleKey)
+    }?.bundle ?: return null
+
+    val allowedAuthConstantKey = "androidx.credentials.provider.BUNDLE_HINT_ALLOWED_AUTHENTICATORS"
+    val cryptoOpIdKey = "androidx.credentials.provider.BUNDLE_HINT_CRYPTO_OP_ID"
+
+    if (!biometricPromptDataBundle.containsKey(allowedAuthConstantKey)) {
+        return null
+    }
+
+    val allowedAuthenticators: Int = biometricPromptDataBundle.getInt(allowedAuthConstantKey)
 
     // This is optional and does not affect validating the biometric flow in any case
-    val opId: Int? = entry.slice.items.firstOrNull {
-        it.hasHint(hintPrefix + "SLICE_HINT_CRYPTO_OP_ID")
-    }?.int
-    if (allowedAuthenticators != null) {
-        return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators)
-    }
-    return null
+    val opId: Long? = if (biometricPromptDataBundle.containsKey(cryptoOpIdKey))
+        biometricPromptDataBundle.getLong(cryptoOpIdKey) else null
+
+    return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators)
 }
 
 val Slice.credentialEntry: CredentialEntry?
diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt
index 486cfe7..fe4bead 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt
@@ -23,6 +23,6 @@
  * null.
  */
 data class BiometricRequestInfo(
-    val opId: Int? = null,
+    val opId: Long? = null,
     val allowedAuthenticators: Int
 )
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 7bc25ed..894d5ef 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -135,16 +135,18 @@
                     Log.w(Constants.LOG_TAG, "Unexpected biometric result exists when " +
                             "autoSelect is preferred.")
                 }
-                // TODO(b/333445754) : Decide whether to propagate info on prompt launch
+                // TODO(b/333445754) : Change the fm option to false in qpr after discussion
                 if (biometricState.biometricResult != null) {
                     entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_RESULT,
                         biometricState.biometricResult.biometricAuthenticationResult
                             .authenticationType)
+                    entryIntent?.putExtra(Constants.BIOMETRIC_FRAMEWORK_OPTION, true)
                 } else if (biometricState.biometricError != null){
                     entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_ERROR_CODE,
                         biometricState.biometricError.errorCode)
                     entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_ERROR_MESSAGE,
                         biometricState.biometricError.errorMessage)
+                    entryIntent?.putExtra(Constants.BIOMETRIC_FRAMEWORK_OPTION, true)
                 }
             }
             val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index b43b5f3..c35721c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
@@ -38,7 +38,6 @@
 import com.android.credentialmanager.model.creation.CreateOptionInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.model.get.ProviderInfo
-import java.lang.Exception
 
 /**
  * Aggregates common display information used for the Biometric Flow.
@@ -121,11 +120,11 @@
     getBiometricCancellationSignal: () -> CancellationSignal,
     getRequestDisplayInfo: RequestDisplayInfo? = null,
     getProviderInfoList: List<ProviderInfo>? = null,
-    getProviderDisplayInfo: ProviderDisplayInfo? = null,
-) {
+    getProviderDisplayInfo: ProviderDisplayInfo? = null
+): Boolean {
     if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
         // Screen is already up, do not re-launch
-        return
+        return false
     }
     onBiometricPromptStateChange(BiometricPromptState.PENDING)
     val biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(
@@ -137,7 +136,7 @@
 
     if (biometricDisplayInfo == null) {
         onBiometricFailureFallback(BiometricFlowType.GET)
-        return
+        return false
     }
 
     val callback: BiometricPrompt.AuthenticationCallback =
@@ -146,7 +145,7 @@
             getBiometricPromptState)
 
     Log.d(TAG, "The BiometricPrompt API call begins for Get.")
-    runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
+    return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
         onBiometricFailureFallback, BiometricFlowType.GET, onCancelFlowAndFinish,
         getBiometricCancellationSignal)
 }
@@ -169,11 +168,11 @@
     getBiometricCancellationSignal: () -> CancellationSignal,
     createRequestDisplayInfo: com.android.credentialmanager.createflow
     .RequestDisplayInfo? = null,
-    createProviderInfo: EnabledProviderInfo? = null,
-) {
+    createProviderInfo: EnabledProviderInfo? = null
+): Boolean {
     if (getBiometricPromptState() != BiometricPromptState.INACTIVE) {
         // Screen is already up, do not re-launch
-        return
+        return false
     }
     onBiometricPromptStateChange(BiometricPromptState.PENDING)
     val biometricDisplayInfo = validateAndRetrieveBiometricCreateDisplayInfo(
@@ -184,7 +183,7 @@
 
     if (biometricDisplayInfo == null) {
         onBiometricFailureFallback(BiometricFlowType.CREATE)
-        return
+        return false
     }
 
     val callback: BiometricPrompt.AuthenticationCallback =
@@ -193,7 +192,7 @@
             getBiometricPromptState)
 
     Log.d(TAG, "The BiometricPrompt API call begins for Create.")
-    runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
+    return runBiometricFlow(context, biometricDisplayInfo, callback, openMoreOptionsPage,
         onBiometricFailureFallback, BiometricFlowType.CREATE, onCancelFlowAndFinish,
         getBiometricCancellationSignal)
 }
@@ -206,19 +205,19 @@
  * only device credentials are requested.
  */
 private fun runBiometricFlow(
-    context: Context,
-    biometricDisplayInfo: BiometricDisplayInfo,
-    callback: BiometricPrompt.AuthenticationCallback,
-    openMoreOptionsPage: () -> Unit,
-    onBiometricFailureFallback: (BiometricFlowType) -> Unit,
-    biometricFlowType: BiometricFlowType,
-    onCancelFlowAndFinish: () -> Unit,
-    getBiometricCancellationSignal: () -> CancellationSignal,
-) {
+        context: Context,
+        biometricDisplayInfo: BiometricDisplayInfo,
+        callback: BiometricPrompt.AuthenticationCallback,
+        openMoreOptionsPage: () -> Unit,
+        onBiometricFailureFallback: (BiometricFlowType) -> Unit,
+        biometricFlowType: BiometricFlowType,
+        onCancelFlowAndFinish: () -> Unit,
+        getBiometricCancellationSignal: () -> CancellationSignal
+): Boolean {
     try {
         if (!canCallBiometricPrompt(biometricDisplayInfo, context)) {
             onBiometricFailureFallback(biometricFlowType)
-            return
+            return false
         }
 
         val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo,
@@ -231,7 +230,7 @@
         val cryptoOpId = getCryptoOpId(biometricDisplayInfo)
         if (cryptoOpId != null) {
             biometricPrompt.authenticate(
-                BiometricPrompt.CryptoObject(cryptoOpId.toLong()),
+                BiometricPrompt.CryptoObject(cryptoOpId),
                 cancellationSignal, executor, callback)
         } else {
             biometricPrompt.authenticate(cancellationSignal, executor, callback)
@@ -239,10 +238,12 @@
     } catch (e: IllegalArgumentException) {
         Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n")
         onBiometricFailureFallback(biometricFlowType)
+        return false
     }
+    return true
 }
 
-private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Int? {
+private fun getCryptoOpId(biometricDisplayInfo: BiometricDisplayInfo): Long? {
     return biometricDisplayInfo.biometricRequestInfo.opId
 }
 
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
index 3c80113..cb089ad 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt
@@ -22,9 +22,12 @@
         const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
             "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED"
         const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED"
-        // TODO(b/333445772) : Qualify error codes fully for propagation
-        const val BIOMETRIC_AUTH_RESULT = "BIOMETRIC_AUTH_RESULT"
-        const val BIOMETRIC_AUTH_ERROR_CODE = "BIOMETRIC_AUTH_ERROR_CODE"
-        const val BIOMETRIC_AUTH_ERROR_MESSAGE = "BIOMETRIC_AUTH_ERROR_MESSAGE"
+        const val BIOMETRIC_AUTH_RESULT = "androidx.credentials.provider.BIOMETRIC_AUTH_RESULT"
+        const val BIOMETRIC_AUTH_ERROR_CODE =
+                "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_CODE"
+        const val BIOMETRIC_AUTH_ERROR_MESSAGE =
+                "androidx.credentials.provider.BIOMETRIC_AUTH_ERROR_MESSAGE"
+        const val BIOMETRIC_FRAMEWORK_OPTION =
+                "androidx.credentials.provider.BIOMETRIC_FRAMEWORK_OPTION"
     }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 7d61f73..4993a1f 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -123,7 +123,8 @@
                                 onBiometricPromptStateChange =
                                 viewModel::onBiometricPromptStateChange,
                                 getBiometricCancellationSignal =
-                                viewModel::getBiometricCancellationSignal
+                                viewModel::getBiometricCancellationSignal,
+                                onLog = { viewModel.logUiEvent(it) },
                             )
                         CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard(
                                 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
@@ -642,12 +643,13 @@
     getBiometricPromptState: () -> BiometricPromptState,
     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
     getBiometricCancellationSignal: () -> CancellationSignal,
+    onLog: @Composable (UiEventEnum) -> Unit
 ) {
     if (biometricEntry == null) {
         fallbackToOriginalFlow(BiometricFlowType.CREATE)
         return
     }
-    runBiometricFlowForCreate(
+    val biometricFlowCalled = runBiometricFlowForCreate(
         biometricEntry = biometricEntry,
         context = LocalContext.current,
         openMoreOptionsPage = onMoreOptionSelected,
@@ -659,6 +661,9 @@
         createProviderInfo = enabledProviderInfo,
         onBiometricFailureFallback = fallbackToOriginalFlow,
         onIllegalStateAndFinish = onIllegalScreenStateAndFinish,
-        getBiometricCancellationSignal = getBiometricCancellationSignal,
+        getBiometricCancellationSignal = getBiometricCancellationSignal
     )
+    if (biometricFlowCalled) {
+        onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED)
+    }
 }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index ba61b90..517ad00 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -166,7 +166,8 @@
                                 onBiometricPromptStateChange =
                                 viewModel::onBiometricPromptStateChange,
                                 getBiometricCancellationSignal =
-                                viewModel::getBiometricCancellationSignal
+                                viewModel::getBiometricCancellationSignal,
+                                onLog = { viewModel.logUiEvent(it) },
                             )
                         } else if (credmanBiometricApiEnabled() &&
                                 getCredentialUiState.currentScreenState
@@ -260,12 +261,13 @@
     getBiometricPromptState: () -> BiometricPromptState,
     onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
     getBiometricCancellationSignal: () -> CancellationSignal,
+    onLog: @Composable (UiEventEnum) -> Unit,
 ) {
     if (biometricEntry == null) {
         fallbackToOriginalFlow(BiometricFlowType.GET)
         return
     }
-    runBiometricFlowForGet(
+    val biometricFlowCalled = runBiometricFlowForGet(
         biometricEntry = biometricEntry,
         context = LocalContext.current,
         openMoreOptionsPage = onMoreOptionSelected,
@@ -280,6 +282,9 @@
         onBiometricFailureFallback = fallbackToOriginalFlow,
         getBiometricCancellationSignal = getBiometricCancellationSignal
     )
+    if (biometricFlowCalled) {
+        onLog(GetCredentialEvent.CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED)
+    }
 }
 
 /** Draws the primary credential selection page, used in Android U. */
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
index daa42be..dac25fa 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
@@ -52,7 +52,10 @@
     CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION(1327),
 
     @UiEvent(doc = "The more about passkeys intro card is visible on screen.")
-    CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328);
+    CREDMAN_CREATE_CRED_MORE_ABOUT_PASSKEYS_INTRO(1328),
+
+    @UiEvent(doc = "The single tap biometric flow is launched.")
+    CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED(1800);
 
     override fun getId(): Int {
         return this.id
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
index 8de8895..8870f28 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
@@ -54,7 +54,10 @@
     CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD(1341),
 
     @UiEvent(doc = "The all sign in option card is visible on screen.")
-    CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342);
+    CREDMAN_GET_CRED_ALL_SIGN_IN_OPTION_CARD(1342),
+
+    @UiEvent(doc = "The single tap biometric flow is launched.")
+    CREDMAN_GET_CRED_BIOMETRIC_FLOW_LAUNCHED(1801);
 
     override fun getId(): Int {
         return this.id
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
index 59a511d..10e8246 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java
@@ -311,18 +311,18 @@
                         broadcastIntent,
                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
 
-                try {
-                    // Delay committing the session by 100ms to fix a UI glitch while displaying the
-                    // Update-Owner change dialog on top of the Installing dialog
-                    new Handler(Looper.getMainLooper()).postDelayed(() -> {
+                // Delay committing the session by 100ms to fix a UI glitch while displaying the
+                // Update-Owner change dialog on top of the Installing dialog
+                new Handler(Looper.getMainLooper()).postDelayed(() -> {
+                    try {
                         session.commit(pendingIntent.getIntentSender());
-                    }, 100);
-                } catch (Exception e) {
-                    Log.e(LOG_TAG, "Cannot install package: ", e);
-                    launchFailure(PackageInstaller.STATUS_FAILURE,
-                        PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
-                    return;
-                }
+                    } catch (Exception e) {
+                        Log.e(LOG_TAG, "Cannot install package: ", e);
+                        launchFailure(PackageInstaller.STATUS_FAILURE,
+                            PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
+                        return;
+                    }
+                }, 100);
                 mCancelButton.setEnabled(false);
                 setFinishOnTouchOutside(false);
             } else {
diff --git a/packages/SettingsLib/Color/res/values/colors.xml b/packages/SettingsLib/Color/res/values/colors.xml
index ef0dd1b..7097523 100644
--- a/packages/SettingsLib/Color/res/values/colors.xml
+++ b/packages/SettingsLib/Color/res/values/colors.xml
@@ -17,7 +17,6 @@
 
 <resources>
     <!-- Dynamic colors-->
-    <color name="settingslib_color_blue700">#0B57D0</color>
     <color name="settingslib_color_blue600">#1a73e8</color>
     <color name="settingslib_color_blue400">#669df6</color>
     <color name="settingslib_color_blue300">#8ab4f8</color>
@@ -63,4 +62,5 @@
     <color name="settingslib_color_cyan400">#4ecde6</color>
     <color name="settingslib_color_cyan300">#78d9ec</color>
     <color name="settingslib_color_cyan100">#cbf0f8</color>
+    <color name="settingslib_color_charcoal">#171717</color>
 </resources>
diff --git a/packages/SettingsLib/IllustrationPreference/Android.bp b/packages/SettingsLib/IllustrationPreference/Android.bp
index c3a91a2..cd8f584 100644
--- a/packages/SettingsLib/IllustrationPreference/Android.bp
+++ b/packages/SettingsLib/IllustrationPreference/Android.bp
@@ -47,6 +47,7 @@
     aconfig_declarations: "settingslib_illustrationpreference_flags",
 
     min_sdk_version: "30",
+    sdk_version: "system_current",
 
     apex_available: [
         "//apex_available:platform",
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index bc3488fc..cf645f7 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -41,6 +41,9 @@
     static {
         HashMap<String, Integer> map = new HashMap<>();
         map.put(
+                ".grey200",
+                R.color.settingslib_color_grey800);
+        map.put(
                 ".grey600",
                 R.color.settingslib_color_grey400);
         map.put(
@@ -56,9 +59,6 @@
                 ".black",
                 android.R.color.white);
         map.put(
-                ".blue200",
-                R.color.settingslib_color_blue700);
-        map.put(
                 ".blue400",
                 R.color.settingslib_color_blue600);
         map.put(
@@ -70,6 +70,9 @@
         map.put(
                 ".red200",
                 R.color.settingslib_color_red500);
+        map.put(
+                ".cream",
+                R.color.settingslib_color_charcoal);
         DARK_TO_LIGHT_THEME_COLOR_MAP = Collections.unmodifiableMap(map);
     }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt
index a6cc3a9..ea4480f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Lottie.kt
@@ -49,6 +49,7 @@
 
 object LottieColorUtils {
     private val DARK_TO_LIGHT_THEME_COLOR_MAP = mapOf(
+        ".grey200" to R.color.settingslib_color_grey800,
         ".grey600" to R.color.settingslib_color_grey400,
         ".grey800" to R.color.settingslib_color_grey300,
         ".grey900" to R.color.settingslib_color_grey50,
@@ -58,6 +59,7 @@
         ".green400" to R.color.settingslib_color_green600,
         ".green200" to R.color.settingslib_color_green500,
         ".red200" to R.color.settingslib_color_red500,
+        ".cream" to R.color.settingslib_color_charcoal
     )
 
     @Composable
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
index 71072a5..d91c7e6 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
@@ -16,9 +16,9 @@
 
 package com.android.settingslib.spa.widget.ui
 
-import android.content.ClipData
-import android.content.ClipboardManager
 import android.content.Context
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.longClick
@@ -63,9 +63,9 @@
 
     @Test
     fun onCopy_saveToClipboard() {
-        val clipboardManager = context.getSystemService(ClipboardManager::class.java)!!
-        clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
+        var clipboardManager: ClipboardManager? = null
         composeTestRule.setContent {
+            clipboardManager = LocalClipboardManager.current
             CopyableBody(TEXT)
         }
 
@@ -74,7 +74,7 @@
         }
         composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).performClick()
 
-        assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(TEXT)
+        assertThat(clipboardManager?.getText()?.text).isEqualTo(TEXT)
     }
 
     private companion object {
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index ab04904..470cdee 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -32,7 +32,7 @@
 
     <!-- Usage graph dimens -->
     <dimen name="usage_graph_margin_top_bottom">9dp</dimen>
-    <dimen name="usage_graph_labels_width">56dp</dimen>
+    <dimen name="usage_graph_labels_width">60dp</dimen>
 
     <dimen name="usage_graph_divider_size">1dp</dimen>
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 869fb7f..7081195 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -81,7 +81,7 @@
                     localMediaManager.unregisterCallback(callback)
                 }
             }
-            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
+            .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 0)
 
     override val currentConnectedDevice: StateFlow<MediaDevice?> =
         merge(devicesChanges, mediaDevicesUpdates)
@@ -89,8 +89,8 @@
             .onStart { emit(localMediaManager.currentConnectedDevice) }
             .stateIn(
                 coroutineScope,
-                SharingStarted.WhileSubscribed(),
-                localMediaManager.currentConnectedDevice
+                SharingStarted.Eagerly,
+                localMediaManager.currentConnectedDevice,
             )
 
     private sealed interface DevicesUpdate {
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index f83928d..03c2a83 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -418,6 +418,7 @@
         VALIDATORS.put(Global.Wearable.CHARGING_SOUNDS_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.BEDTIME_MODE, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.BEDTIME_HARD_MODE, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Global.Wearable.VIBRATE_FOR_ACTIVE_UNLOCK, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.DYNAMIC_COLOR_THEME_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.SCREENSHOT_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.UPGRADE_DATA_MIGRATION_STATUS,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 473955f..4ec170d 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -619,6 +619,7 @@
                     Settings.Global.Wearable.COOLDOWN_MODE_ON,
                     Settings.Global.Wearable.BEDTIME_MODE,
                     Settings.Global.Wearable.BEDTIME_HARD_MODE,
+                    Settings.Global.Wearable.VIBRATE_FOR_ACTIVE_UNLOCK,
                     Settings.Global.Wearable.LOCK_SCREEN_STATE,
                     Settings.Global.Wearable.DISABLE_AOD_WHILE_PLUGGED,
                     Settings.Global.Wearable.NETWORK_LOCATION_OPT_IN,
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java
index ca1e4c1..e4898da 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java
@@ -27,11 +27,11 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.bedstead.enterprise.annotations.EnsureHasNoWorkProfile;
+import com.android.bedstead.enterprise.annotations.EnsureHasWorkProfile;
 import com.android.bedstead.harrier.BedsteadJUnit4;
 import com.android.bedstead.harrier.DeviceState;
-import com.android.bedstead.harrier.annotations.EnsureHasNoWorkProfile;
 import com.android.bedstead.harrier.annotations.EnsureHasSecondaryUser;
-import com.android.bedstead.harrier.annotations.EnsureHasWorkProfile;
 import com.android.bedstead.harrier.annotations.RequireFeature;
 import com.android.bedstead.harrier.annotations.RequireRunOnInitialUser;
 import com.android.bedstead.harrier.annotations.RequireRunOnPrimaryUser;
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e940674..7a098ee 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -78,9 +78,42 @@
     visibility: ["//visibility:private"],
 }
 
+// Tests where robolectric conversion caused errors in SystemUITests at runtime
+filegroup {
+    name: "SystemUI-tests-broken-robofiles-sysui-run",
+    srcs: [
+        "tests/src/**/systemui/globalactions/GlobalActionsDialogLiteTest.java",
+        "tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt",
+        "tests/src/**/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt",
+        "tests/src/**/systemui/media/dialog/MediaOutputAdapterTest.java",
+        "tests/src/**/systemui/media/dialog/MediaOutputBaseDialogTest.java",
+        "tests/src/**/systemui/media/dialog/MediaOutputBroadcastDialogTest.java",
+        "tests/src/**/systemui/media/dialog/MediaOutputDialogTest.java",
+        "tests/src/**/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegateTest.kt",
+    ],
+}
+
 filegroup {
     name: "SystemUI-tests-broken-robofiles-run",
     srcs: [
+        "tests/src/**/systemui/keyguard/CustomizationProviderTest.kt",
+        "tests/src/**/systemui/globalactions/GlobalActionsColumnLayoutTest.java",
+        "tests/src/**/systemui/globalactions/GlobalActionsDialogLiteTest.java",
+        "tests/src/**/systemui/globalactions/GlobalActionsImeTest.java",
+        "tests/src/**/systemui/graphics/ImageLoaderTest.kt",
+        "tests/src/**/systemui/keyguard/KeyguardViewMediatorTest.java",
+        "tests/src/**/systemui/keyguard/LifecycleTest.java",
+        "tests/src/**/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt",
+        "tests/src/**/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt",
+        "tests/src/**/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt",
+        "tests/src/**/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt",
+        "tests/src/**/systemui/lifecycle/RepeatWhenAttachedTest.kt",
+        "tests/src/**/systemui/log/LogBufferTest.kt",
+        "tests/src/**/systemui/media/dialog/MediaOutputBaseDialogTest.java",
+        "tests/src/**/systemui/media/dialog/MediaOutputBroadcastDialogTest.java",
+        "tests/src/**/systemui/media/dialog/MediaOutputDialogTest.java",
+        "tests/src/**/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt",
+        "tests/src/**/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt",
         "tests/src/**/systemui/util/LifecycleFragmentTest.java",
         "tests/src/**/systemui/util/TestableAlertDialogTest.kt",
         "tests/src/**/systemui/util/kotlin/PairwiseFlowTest",
@@ -767,6 +800,7 @@
         ":SystemUI-tests-robofiles",
     ],
     exclude_srcs: [
+        ":SystemUI-tests-broken-robofiles-sysui-run",
         ":SystemUI-tests-broken-robofiles-compile",
         ":SystemUI-tests-broken-robofiles-run",
     ],
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7ce8f98..3310cfa 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -177,16 +177,9 @@
 }
 
 flag {
-    name: "notification_throttle_hun"
-    namespace: "systemui"
-    description: "During notification avalanche, throttle HUNs showing in fast succession."
-    bug: "307288824"
-}
-
-flag {
     name: "notification_avalanche_throttle_hun"
     namespace: "systemui"
-    description: "(currently unused) During notification avalanche, throttle HUNs showing in fast succession."
+    description: "During notification avalanche, throttle HUNs showing in fast succession."
     bug: "307288824"
 }
 
@@ -743,16 +736,6 @@
 }
 
 flag {
-    name: "trim_resources_with_background_trim_at_lock"
-    namespace: "systemui"
-    description: "Trim fonts and other caches when the device locks to lower memory consumption."
-    bug: "322143614"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
     name: "dedicated_notif_inflation_thread"
     namespace: "systemui"
     description: "Create a separate background thread for inflating notifications"
@@ -997,6 +980,13 @@
 }
 
 flag {
+  name: "glanceable_hub_fullscreen_swipe"
+  namespace: "systemui"
+  description: "Increase swipe area for gestures to bring in glanceable hub"
+  bug: "339665673"
+}
+
+flag {
   name: "glanceable_hub_shortcut_button"
   namespace: "systemui"
   description: "Shows a button over the dream and lock screen to open the glanceable hub"
@@ -1011,6 +1001,13 @@
 }
 
 flag {
+  name: "glanceable_hub_allow_keyguard_when_dreaming"
+  namespace: "systemui"
+  description: "Allows users to exit dream to keyguard with glanceable hub enabled"
+  bug: "343505271"
+}
+
+flag {
   name: "new_touchpad_gestures_tutorial"
   namespace: "systemui"
   description: "Enables new interactive tutorial for learning touchpad gestures"
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt
deleted file mode 100644
index dff8753..0000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/platform/DensityAwareComposeView.kt
+++ /dev/null
@@ -1,96 +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.compose.ui.platform
-
-import android.content.Context
-import android.content.res.Configuration
-import android.util.AttributeSet
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.platform.AbstractComposeView
-
-/**
- * A ComposeView that recreates its composition if the display size or font scale was changed.
- *
- * TODO(b/317317814): Remove this workaround.
- */
-class DensityAwareComposeView(context: Context) : OpenComposeView(context) {
-    private var lastDensityDpi: Int = -1
-    private var lastFontScale: Float = -1f
-
-    override fun onAttachedToWindow() {
-        super.onAttachedToWindow()
-
-        val configuration = context.resources.configuration
-        lastDensityDpi = configuration.densityDpi
-        lastFontScale = configuration.fontScale
-    }
-
-    override fun dispatchConfigurationChanged(newConfig: Configuration) {
-        super.dispatchConfigurationChanged(newConfig)
-
-        // If the density or font scale changed, we dispose then recreate the composition. Note that
-        // we do this here after dispatching the new configuration to children (instead of doing
-        // this in onConfigurationChanged()) because the new configuration should first be
-        // dispatched to the AndroidComposeView that holds the current density before we recreate
-        // the composition.
-        val densityDpi = newConfig.densityDpi
-        val fontScale = newConfig.fontScale
-        if (densityDpi != lastDensityDpi || fontScale != lastFontScale) {
-            lastDensityDpi = densityDpi
-            lastFontScale = fontScale
-
-            disposeComposition()
-            if (isAttachedToWindow) {
-                createComposition()
-            }
-        }
-    }
-}
-
-/** A fork of [androidx.compose.ui.platform.ComposeView] that is open and can be subclassed. */
-open class OpenComposeView
-internal constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
-    AbstractComposeView(context, attrs, defStyleAttr) {
-
-    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
-
-    @Suppress("RedundantVisibilityModifier")
-    protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
-
-    @Composable
-    override fun Content() {
-        content.value?.invoke()
-    }
-
-    override fun getAccessibilityClassName(): CharSequence {
-        return javaClass.name
-    }
-
-    /**
-     * Set the Jetpack Compose UI content for this view. Initial composition will occur when the
-     * view becomes attached to a window or when [createComposition] is called, whichever comes
-     * first.
-     */
-    fun setContent(content: @Composable () -> Unit) {
-        shouldCreateCompositionOnAttachedToWindow = true
-        this.content.value = content
-        if (isAttachedToWindow) {
-            createComposition()
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index feb1f5b..a90f82e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -21,6 +21,8 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.CommunalSwipeDetector
+import com.android.compose.animation.scene.DefaultSwipeDetector
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.ElementMatcher
@@ -35,6 +37,7 @@
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.transitions
 import com.android.systemui.Flags
+import com.android.systemui.Flags.glanceableHubFullscreenSwipe
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.CommunalTransitionKeys
 import com.android.systemui.communal.ui.compose.extensions.allowGestures
@@ -108,6 +111,8 @@
         )
     }
 
+    val detector = remember { CommunalSwipeDetector() }
+
     DisposableEffect(state) {
         val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
         dataSourceDelegator.setDelegate(dataSource)
@@ -121,13 +126,25 @@
         onDispose { viewModel.setTransitionState(null) }
     }
 
+    val swipeSourceDetector =
+        if (glanceableHubFullscreenSwipe()) {
+            detector
+        } else {
+            FixedSizeEdgeDetector(dimensionResource(id = R.dimen.communal_gesture_initiation_width))
+        }
+
+    val swipeDetector =
+        if (glanceableHubFullscreenSwipe()) {
+            detector
+        } else {
+            DefaultSwipeDetector
+        }
+
     SceneTransitionLayout(
         state = state,
         modifier = modifier.fillMaxSize(),
-        swipeSourceDetector =
-            FixedSizeEdgeDetector(
-                dimensionResource(id = R.dimen.communal_gesture_initiation_width)
-            ),
+        swipeSourceDetector = swipeSourceDetector,
+        swipeDetector = swipeDetector,
     ) {
         scene(
             CommunalScenes.Blank,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 9dd3d39..1f7f07b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -28,7 +28,9 @@
 import androidx.compose.animation.AnimatedVisibilityScope
 import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.Spring
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
@@ -445,6 +447,14 @@
                 val selected by
                     remember(index) { derivedStateOf { list[index].key == selectedKey.value } }
                 DraggableItem(
+                    modifier =
+                        if (dragDropState.draggingItemIndex == index) {
+                            Modifier
+                        } else {
+                            Modifier.animateItem(
+                                placementSpec = spring(stiffness = Spring.StiffnessMediumLow)
+                            )
+                        },
                     dragDropState = dragDropState,
                     selected = selected,
                     enabled = list[index].isWidgetContent(),
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 c26259f..ee3ffce 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
@@ -126,7 +126,8 @@
                             " size=${coordinates.size}" +
                             " bounds=$boundsInWindow"
                     }
-                    viewModel.onHeadsUpTopChanged(boundsInWindow.top)
+                    // Note: boundsInWindow doesn't scroll off the screen
+                    stackScrollView.setHeadsUpTop(boundsInWindow.top)
                 }
     ) {
         content {}
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 ae53d56..7ca68fb 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
@@ -18,8 +18,8 @@
 
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
@@ -75,7 +75,7 @@
         OverlayShade(
             modifier = modifier,
             viewModel = overlayShadeViewModel,
-            horizontalArrangement = Arrangement.Start,
+            horizontalArrangement = Arrangement.End,
             lockscreenContent = lockscreenContent,
         ) {
             Column {
@@ -95,7 +95,7 @@
                     shouldPunchHoleBehindScrim = false,
                     shouldFillMaxSize = false,
                     shadeMode = ShadeMode.Dual,
-                    modifier = Modifier.width(416.dp),
+                    modifier = Modifier.fillMaxWidth(),
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
index 975829a..efda4cd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt
@@ -17,17 +17,28 @@
 package com.android.systemui.scene.ui.composable
 
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.absoluteOffset
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.IntOffset
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.compose.animation.scene.animateSceneFloatAsState
+import com.android.internal.policy.SystemBarUtils
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
 import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.viewmodel.GoneSceneViewModel
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
@@ -39,6 +50,8 @@
 class GoneScene
 @Inject
 constructor(
+    private val notificationStackScrolLView: Lazy<NotificationScrollView>,
+    private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
     private val viewModel: GoneSceneViewModel,
 ) : ComposableScene {
     override val key = Scenes.Gone
@@ -55,5 +68,28 @@
             key = QuickSettings.SharedValues.TilesSquishiness,
         )
         Spacer(modifier.fillMaxSize())
+        HeadsUpNotificationStack(
+            stackScrollView = notificationStackScrolLView.get(),
+            viewModel = notificationsPlaceholderViewModel
+        )
     }
 }
+
+@Composable
+private fun SceneScope.HeadsUpNotificationStack(
+    stackScrollView: NotificationScrollView,
+    viewModel: NotificationsPlaceholderViewModel,
+) {
+    val context = LocalContext.current
+    val density = LocalDensity.current
+    val statusBarHeight = SystemBarUtils.getStatusBarHeight(context)
+    val headsUpPadding =
+        with(density) { dimensionResource(id = R.dimen.heads_up_status_bar_padding).roundToPx() }
+
+    HeadsUpNotificationSpace(
+        stackScrollView = stackScrollView,
+        viewModel = viewModel,
+        modifier =
+            Modifier.absoluteOffset { IntOffset(x = 0, y = statusBarHeight + headsUpPadding) }
+    )
+}
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 f5a0ef2..3255b08 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
@@ -22,6 +22,7 @@
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToSplitShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.shadeToQuickSettingsTransition
+import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.composable.Shade
 
 /**
@@ -102,4 +103,10 @@
             y = { Shade.Dimensions.ScrimOverscrollLimit }
         )
     }
+    overscroll(Scenes.NotificationsShade, Orientation.Vertical) {
+        translate(OverlayShade.Elements.Panel, y = { OverlayShade.Dimensions.OverscrollLimit })
+    }
+    overscroll(Scenes.QuickSettingsShade, Orientation.Vertical) {
+        translate(OverlayShade.Elements.Panel, y = { OverlayShade.Dimensions.OverscrollLimit })
+    }
 }
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
index a6b268d..6b3b760 100644
--- 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
@@ -50,8 +50,7 @@
             }
         }
 
-    translate(OverlayShade.Elements.PanelBackground, Edge.Top)
-    translate(Notifications.Elements.NotificationScrim, Edge.Top)
+    translate(OverlayShade.Elements.Panel, Edge.Top)
 
     fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
 
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
index 2baaecf..ec2f14f 100644
--- 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
@@ -48,7 +48,7 @@
             }
         }
 
-    translate(OverlayShade.Elements.PanelBackground, Edge.Top)
+    translate(OverlayShade.Elements.Panel, Edge.Top)
 
     fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
 }
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 34cc676..d776e2a 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
@@ -14,17 +14,28 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalLayoutApi::class)
+
 package com.android.systemui.shade.ui.composable
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsIgnoringVisibility
+import androidx.compose.foundation.layout.waterfall
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
@@ -35,12 +46,12 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 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
@@ -59,8 +70,6 @@
     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) {
@@ -73,13 +82,13 @@
         Scrim(onClicked = viewModel::onScrimClicked)
 
         Row(
-            modifier =
-                Modifier.fillMaxSize().thenIf(!isPanelFullWidth) {
-                    Modifier.padding(OverlayShade.Dimensions.ScrimContentPadding)
-                },
+            modifier = Modifier.fillMaxSize().panelPadding(),
             horizontalArrangement = horizontalArrangement,
         ) {
-            Panel(modifier = Modifier.panelSize(), content = content)
+            Panel(
+                modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(),
+                content = content
+            )
         }
     }
 }
@@ -135,9 +144,46 @@
     )
 }
 
+@Composable
+private fun Modifier.panelPadding(): Modifier {
+    val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
+    val systemBars = WindowInsets.systemBarsIgnoringVisibility
+    val displayCutout = WindowInsets.displayCutout
+    val waterfall = WindowInsets.waterfall
+    val contentPadding = PaddingValues(all = OverlayShade.Dimensions.ScrimContentPadding)
+
+    val combinedPadding =
+        combinePaddings(
+            systemBars.asPaddingValues(),
+            displayCutout.asPaddingValues(),
+            waterfall.asPaddingValues(),
+            contentPadding
+        )
+
+    return if (widthSizeClass == WindowWidthSizeClass.Compact) {
+        padding(bottom = combinedPadding.calculateBottomPadding())
+    } else {
+        padding(combinedPadding)
+    }
+}
+
+/** Creates a union of [paddingValues] by using the max padding of each edge. */
+@Composable
+private fun combinePaddings(vararg paddingValues: PaddingValues): PaddingValues {
+    val layoutDirection = LocalLayoutDirection.current
+
+    return PaddingValues(
+        start = paddingValues.maxOfOrNull { it.calculateStartPadding(layoutDirection) } ?: 0.dp,
+        top = paddingValues.maxOfOrNull { it.calculateTopPadding() } ?: 0.dp,
+        end = paddingValues.maxOfOrNull { it.calculateEndPadding(layoutDirection) } ?: 0.dp,
+        bottom = paddingValues.maxOfOrNull { it.calculateBottomPadding() } ?: 0.dp
+    )
+}
+
 object OverlayShade {
     object Elements {
         val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker)
+        val Panel = ElementKey("OverlayShadePanel", scenePicker = LowestZIndexScenePicker)
         val PanelBackground =
             ElementKey("OverlayShadePanelBackground", scenePicker = LowestZIndexScenePicker)
     }
@@ -153,6 +199,7 @@
         val PanelCornerRadius = 46.dp
         val PanelWidthMedium = 390.dp
         val PanelWidthLarge = 474.dp
+        val OverscrollLimit = 100f
     }
 
     object Shapes {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
index da29d58..48af8cd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
@@ -16,16 +16,13 @@
 
 package com.android.systemui.volume.panel.component.spatialaudio
 
-import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent
 import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
 import com.android.systemui.volume.panel.component.spatial.domain.SpatialAudioAvailabilityCriteria
-import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel
-import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioPopup
+import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioComponent
 import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
 import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
 import dagger.Binds
 import dagger.Module
-import dagger.Provides
 import dagger.multibindings.IntoMap
 import dagger.multibindings.StringKey
 
@@ -40,14 +37,8 @@
         criteria: SpatialAudioAvailabilityCriteria
     ): ComponentAvailabilityCriteria
 
-    companion object {
-
-        @Provides
-        @IntoMap
-        @StringKey(VolumePanelComponents.SPATIAL_AUDIO)
-        fun provideVolumePanelUiComponent(
-            viewModel: SpatialAudioViewModel,
-            popup: SpatialAudioPopup,
-        ): VolumePanelUiComponent = ButtonComponent(viewModel.spatialAudioButton, popup::show)
-    }
+    @Binds
+    @IntoMap
+    @StringKey(VolumePanelComponents.SPATIAL_AUDIO)
+    fun bindVolumePanelUiComponent(component: SpatialAudioComponent): VolumePanelUiComponent
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt
new file mode 100644
index 0000000..2d89b5c
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.component.spatialaudio.ui.composable
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent
+import com.android.systemui.volume.panel.component.button.ui.composable.ToggleButtonComponent
+import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel
+import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel
+import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
+import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
+import javax.inject.Inject
+
+/** [ComposeVolumePanelUiComponent] that represents spatial audio button in the Volume Panel. */
+class SpatialAudioComponent
+@Inject
+constructor(
+    private val viewModel: SpatialAudioViewModel,
+    private val popup: SpatialAudioPopup,
+) : ComposeVolumePanelUiComponent {
+
+    @Composable
+    override fun VolumePanelComposeScope.Content(modifier: Modifier) {
+        val shouldUsePopup by viewModel.shouldUsePopup.collectAsStateWithLifecycle()
+
+        val buttonComponent: ComposeVolumePanelUiComponent =
+            remember(shouldUsePopup) {
+                if (shouldUsePopup) {
+                    ButtonComponent(viewModel.spatialAudioButton, popup::show)
+                } else {
+                    ToggleButtonComponent(viewModel.spatialAudioButton) {
+                        if (it) {
+                            viewModel.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled)
+                        } else {
+                            viewModel.setEnabled(SpatialAudioEnabledModel.Disabled)
+                        }
+                    }
+                }
+            }
+        with(buttonComponent) { Content(modifier) }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index 271eb96..fbf91b7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -68,9 +68,7 @@
                     state.a11yClickDescription?.let {
                         customActions =
                             listOf(
-                                CustomAccessibilityAction(
-                                    it,
-                                ) {
+                                CustomAccessibilityAction(it) {
                                     onIconTapped()
                                     true
                                 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
index ac5004e..580aba5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.ui.composable
 
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -56,17 +57,19 @@
                     with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
                 }
             }
-            Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.spacedBy(spacing),
-            ) {
-                for (component in layout.footerComponents) {
-                    AnimatedVisibility(
-                        visible = component.isVisible,
-                        modifier = Modifier.weight(1f),
-                    ) {
-                        with(component.component as ComposeVolumePanelUiComponent) {
-                            Content(Modifier)
+            AnimatedContent(
+                targetState = layout.footerComponents,
+                label = "FooterComponentAnimation",
+            ) { footerComponents ->
+                Row(
+                    modifier = Modifier.fillMaxWidth(),
+                    horizontalArrangement = Arrangement.spacedBy(spacing),
+                ) {
+                    for (component in footerComponents) {
+                        if (component.isVisible) {
+                            with(component.component as ComposeVolumePanelUiComponent) {
+                                Content(Modifier.weight(1f))
+                            }
                         }
                     }
                 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
index 9ea20b9..6349c14 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.panel.ui.composable
 
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
@@ -50,26 +51,27 @@
                 with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
             }
         }
-        if (layout.footerComponents.isNotEmpty()) {
+
+        AnimatedContent(
+            targetState = layout.footerComponents,
+            label = "FooterComponentAnimation",
+        ) { footerComponents ->
             Row(
                 modifier = Modifier.fillMaxWidth().wrapContentHeight(),
                 horizontalArrangement = Arrangement.spacedBy(if (isLargeScreen) 28.dp else 20.dp),
             ) {
                 val visibleComponentsCount =
-                    layout.footerComponents.fastSumBy { if (it.isVisible) 1 else 0 }
+                    footerComponents.fastSumBy { if (it.isVisible) 1 else 0 }
 
                 // Center footer component if there is only one present
                 if (visibleComponentsCount == 1) {
                     Spacer(modifier = Modifier.weight(0.5f))
                 }
 
-                for (component in layout.footerComponents) {
-                    AnimatedVisibility(
-                        visible = component.isVisible,
-                        modifier = Modifier.weight(1f),
-                    ) {
+                for (component in footerComponents) {
+                    if (component.isVisible) {
                         with(component.component as ComposeVolumePanelUiComponent) {
-                            Content(Modifier)
+                            Content(Modifier.weight(1f))
                         }
                     }
                 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.kt
new file mode 100644
index 0000000..7be34ca
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/CommunalSwipeDetector.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.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import kotlin.math.abs
+
+private const val TRAVEL_RATIO_THRESHOLD = .5f
+
+/**
+ * {@link CommunalSwipeDetector} provides an implementation of {@link SwipeDetector} and {@link
+ * SwipeSourceDetector} to enable fullscreen swipe handling to transition to and from the glanceable
+ * hub.
+ */
+class CommunalSwipeDetector(private var lastDirection: SwipeSource? = null) :
+    SwipeSourceDetector, SwipeDetector {
+    override fun source(
+        layoutSize: IntSize,
+        position: IntOffset,
+        density: Density,
+        orientation: Orientation
+    ): SwipeSource? {
+        return lastDirection
+    }
+
+    override fun detectSwipe(change: PointerInputChange): Boolean {
+        if (change.positionChange().x > 0) {
+            lastDirection = Edge.Left
+        } else {
+            lastDirection = Edge.Right
+        }
+
+        // Determine whether the ratio of the distance traveled horizontally to the distance
+        // traveled vertically exceeds the threshold.
+        return abs(change.positionChange().x / change.positionChange().y) > TRAVEL_RATIO_THRESHOLD
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 0fc0053..3cc8431 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -72,6 +72,7 @@
     enabled: () -> Boolean,
     startDragImmediately: (startedPosition: Offset) -> Boolean,
     onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
+    swipeDetector: SwipeDetector = DefaultSwipeDetector,
 ): Modifier =
     this.then(
         MultiPointerDraggableElement(
@@ -79,6 +80,7 @@
             enabled,
             startDragImmediately,
             onDragStarted,
+            swipeDetector,
         )
     )
 
@@ -88,6 +90,7 @@
     private val startDragImmediately: (startedPosition: Offset) -> Boolean,
     private val onDragStarted:
         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
+    private val swipeDetector: SwipeDetector,
 ) : ModifierNodeElement<MultiPointerDraggableNode>() {
     override fun create(): MultiPointerDraggableNode =
         MultiPointerDraggableNode(
@@ -95,6 +98,7 @@
             enabled = enabled,
             startDragImmediately = startDragImmediately,
             onDragStarted = onDragStarted,
+            swipeDetector = swipeDetector,
         )
 
     override fun update(node: MultiPointerDraggableNode) {
@@ -102,6 +106,7 @@
         node.enabled = enabled
         node.startDragImmediately = startDragImmediately
         node.onDragStarted = onDragStarted
+        node.swipeDetector = swipeDetector
     }
 }
 
@@ -111,6 +116,7 @@
     var startDragImmediately: (startedPosition: Offset) -> Boolean,
     var onDragStarted:
         (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
+    var swipeDetector: SwipeDetector = DefaultSwipeDetector,
 ) :
     PointerInputModifierNode,
     DelegatingNode(),
@@ -199,6 +205,7 @@
                             onDragCancel = { controller ->
                                 controller.onStop(velocity = 0f, canChangeScene = true)
                             },
+                            swipeDetector = swipeDetector
                         )
                     } catch (exception: CancellationException) {
                         // If the coroutine scope is active, we can just restart the drag cycle.
@@ -226,7 +233,8 @@
             (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController,
         onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit,
         onDragEnd: (controller: DragController) -> Unit,
-        onDragCancel: (controller: DragController) -> Unit
+        onDragCancel: (controller: DragController) -> Unit,
+        swipeDetector: SwipeDetector,
     ) {
         // Wait for a consumable event in [PointerEventPass.Main] pass
         val consumablePointer = awaitConsumableEvent().changes.first()
@@ -238,8 +246,10 @@
                 consumablePointer
             } else {
                 val onSlopReached = { change: PointerInputChange, over: Float ->
-                    change.consume()
-                    overSlop = over
+                    if (swipeDetector.detectSwipe(change)) {
+                        change.consume()
+                        overSlop = over
+                    }
                 }
 
                 // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once it
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 6e29639..2946b04 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -55,6 +55,7 @@
     state: SceneTransitionLayoutState,
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
+    swipeDetector: SwipeDetector = DefaultSwipeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
     scenes: SceneTransitionLayoutScope.() -> Unit,
 ) {
@@ -62,6 +63,7 @@
         state,
         modifier,
         swipeSourceDetector,
+        swipeDetector,
         transitionInterceptionThreshold,
         onLayoutImpl = null,
         scenes,
@@ -95,6 +97,7 @@
     transitions: SceneTransitions,
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
+    swipeDetector: SwipeDetector = DefaultSwipeDetector,
     @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
     enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
     scenes: SceneTransitionLayoutScope.() -> Unit,
@@ -111,6 +114,7 @@
         state,
         modifier,
         swipeSourceDetector,
+        swipeDetector,
         transitionInterceptionThreshold,
         scenes,
     )
@@ -494,6 +498,7 @@
     state: SceneTransitionLayoutState,
     modifier: Modifier = Modifier,
     swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
+    swipeDetector: SwipeDetector = DefaultSwipeDetector,
     transitionInterceptionThreshold: Float = 0f,
     onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
     scenes: SceneTransitionLayoutScope.() -> Unit,
@@ -529,5 +534,5 @@
         layoutImpl.transitionInterceptionThreshold = transitionInterceptionThreshold
     }
 
-    layoutImpl.Content(modifier)
+    layoutImpl.Content(modifier, swipeDetector)
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index f159e27..5fa7c87 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -184,14 +184,14 @@
     }
 
     @Composable
-    internal fun Content(modifier: Modifier) {
+    internal fun Content(modifier: Modifier, swipeDetector: SwipeDetector) {
         Box(
             modifier
                 // Handle horizontal and vertical swipes on this layout.
                 // Note: order here is important and will give a slight priority to the vertical
                 // swipes.
-                .swipeToScene(horizontalDraggableHandler)
-                .swipeToScene(verticalDraggableHandler)
+                .swipeToScene(horizontalDraggableHandler, swipeDetector)
+                .swipeToScene(verticalDraggableHandler, swipeDetector)
                 .then(LayoutElement(layoutImpl = this))
         ) {
             LookaheadScope {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
new file mode 100644
index 0000000..54ee783
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.compose.animation.scene
+
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.input.pointer.PointerInputChange
+
+/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
+@Stable
+interface SwipeDetector {
+    /**
+     * Invoked on changes to pointer input. Returns {@code true} if a swipe has been recognized,
+     * {@code false} otherwise.
+     */
+    fun detectSwipe(change: PointerInputChange): Boolean
+}
+
+val DefaultSwipeDetector = PassthroughSwipeDetector()
+
+/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
+class PassthroughSwipeDetector : SwipeDetector {
+    override fun detectSwipe(change: PointerInputChange): Boolean {
+        // Simply accept all changes as a swipe
+        return true
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index b618369..171e243 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -31,14 +31,18 @@
  * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
  */
 @Stable
-internal fun Modifier.swipeToScene(draggableHandler: DraggableHandlerImpl): Modifier {
-    return this.then(SwipeToSceneElement(draggableHandler))
+internal fun Modifier.swipeToScene(
+    draggableHandler: DraggableHandlerImpl,
+    swipeDetector: SwipeDetector
+): Modifier {
+    return this.then(SwipeToSceneElement(draggableHandler, swipeDetector))
 }
 
 private data class SwipeToSceneElement(
     val draggableHandler: DraggableHandlerImpl,
+    val swipeDetector: SwipeDetector
 ) : ModifierNodeElement<SwipeToSceneNode>() {
-    override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler)
+    override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler, swipeDetector)
 
     override fun update(node: SwipeToSceneNode) {
         node.draggableHandler = draggableHandler
@@ -47,6 +51,7 @@
 
 private class SwipeToSceneNode(
     draggableHandler: DraggableHandlerImpl,
+    swipeDetector: SwipeDetector,
 ) : DelegatingNode(), PointerInputModifierNode {
     private val delegate =
         delegate(
@@ -55,6 +60,7 @@
                 enabled = ::enabled,
                 startDragImmediately = ::startDragImmediately,
                 onDragStarted = draggableHandler::onDragStarted,
+                swipeDetector = swipeDetector,
             )
         )
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
index aa6d113..4bb643f 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalViewConfiguration
@@ -346,4 +347,69 @@
         continueDraggingDown()
         assertThat(stopped).isTrue()
     }
+
+    @Test
+    fun multiPointerSwipeDetectorInteraction() {
+        val size = 200f
+        val middle = Offset(size / 2f, size / 2f)
+
+        var started = false
+
+        var capturedChange: PointerInputChange? = null
+        var swipeConsume = false
+
+        var touchSlop = 0f
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            Box(
+                Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() })
+                    .multiPointerDraggable(
+                        orientation = Orientation.Vertical,
+                        enabled = { true },
+                        startDragImmediately = { false },
+                        swipeDetector =
+                            object : SwipeDetector {
+                                override fun detectSwipe(change: PointerInputChange): Boolean {
+                                    capturedChange = change
+                                    return swipeConsume
+                                }
+                            },
+                        onDragStarted = { _, _, _ ->
+                            started = true
+                            object : DragController {
+                                override fun onDrag(delta: Float) {}
+
+                                override fun onStop(velocity: Float, canChangeScene: Boolean) {}
+                            }
+                        },
+                    )
+            ) {}
+        }
+
+        fun startDraggingDown() {
+            rule.onRoot().performTouchInput {
+                down(middle)
+                moveBy(Offset(0f, touchSlop))
+            }
+        }
+
+        fun continueDraggingDown() {
+            rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) }
+        }
+
+        startDraggingDown()
+        assertThat(capturedChange).isNotNull()
+        capturedChange = null
+        assertThat(started).isFalse()
+
+        swipeConsume = true
+        continueDraggingDown()
+        assertThat(capturedChange).isNotNull()
+        capturedChange = null
+
+        continueDraggingDown()
+        assertThat(capturedChange).isNull()
+
+        assertThat(started).isTrue()
+    }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
index 165e972..de9baa5 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
+++ b/packages/SystemUI/customization/src/com/android/systemui/util/Assert.java
@@ -79,6 +79,21 @@
         }
     }
 
+    /**
+     * Asserts that the current thread is the same as the given thread, or that the current thread
+     * is the test thread.
+     * @param expected The looper we expected to be running on
+     */
+    public static void isCurrentThread(Looper expected) {
+        if (!expected.isCurrentThread()
+                && (sTestThread == null || sTestThread != Thread.currentThread())) {
+            throw new IllegalStateException("Called on wrong thread thread."
+                    + " wanted " + expected.getThread().getName()
+                    + " but instead got Thread.currentThread()="
+                    + Thread.currentThread().getName());
+        }
+    }
+
     public static void isNotMainThread() {
         if (sMainLooper.isCurrentThread()
                 && (sTestThread == null || sTestThread == Thread.currentThread())) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
index e39ad4f..a676c7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/data/repository/ScreenBrightnessDisplayManagerRepositoryTest.kt
@@ -25,15 +25,18 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.brightness.data.model.LinearBrightness
+import com.android.systemui.brightness.shared.model.LinearBrightness
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.testKosmos
 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.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -74,6 +77,8 @@
             ScreenBrightnessDisplayManagerRepository(
                 displayId,
                 displayManager,
+                FakeLogBuffer.Factory.create(),
+                mock<TableLogBuffer>(),
                 kosmos.applicationCoroutineScope,
                 kosmos.testDispatcher,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
index 33c44f8..b6616bf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorTest.kt
@@ -20,13 +20,16 @@
 import androidx.test.filters.SmallTest
 import com.android.settingslib.display.BrightnessUtils
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.brightness.data.model.LinearBrightness
 import com.android.systemui.brightness.data.repository.fakeScreenBrightnessRepository
 import com.android.systemui.brightness.data.repository.screenBrightnessRepository
-import com.android.systemui.brightness.shared.GammaBrightness
+import com.android.systemui.brightness.shared.model.GammaBrightness
+import com.android.systemui.brightness.shared.model.LinearBrightness
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
@@ -41,7 +44,14 @@
 
     private val kosmos = testKosmos()
 
-    private val underTest = ScreenBrightnessInteractor(kosmos.screenBrightnessRepository)
+    private val underTest =
+        with(kosmos) {
+            ScreenBrightnessInteractor(
+                screenBrightnessRepository,
+                applicationCoroutineScope,
+                mock<TableLogBuffer>()
+            )
+        }
 
     @Test
     fun gammaBrightness() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
index 0058ee4..8402676 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
@@ -20,15 +20,16 @@
 import androidx.test.filters.SmallTest
 import com.android.settingslib.display.BrightnessUtils
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.brightness.data.model.LinearBrightness
 import com.android.systemui.brightness.data.repository.fakeScreenBrightnessRepository
 import com.android.systemui.brightness.domain.interactor.brightnessPolicyEnforcementInteractor
 import com.android.systemui.brightness.domain.interactor.screenBrightnessInteractor
-import com.android.systemui.brightness.shared.GammaBrightness
+import com.android.systemui.brightness.shared.model.GammaBrightness
+import com.android.systemui.brightness.shared.model.LinearBrightness
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
@@ -52,6 +53,7 @@
             BrightnessSliderViewModel(
                 screenBrightnessInteractor,
                 brightnessPolicyEnforcementInteractor,
+                applicationCoroutineScope,
             )
         }
 
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 5e19a41..a1bad39 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
@@ -54,6 +54,8 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
@@ -62,6 +64,8 @@
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
 import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.shade.ShadeTestUtil
 import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -71,7 +75,6 @@
 import com.android.systemui.testKosmos
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.data.repository.fakeUserRepository
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -85,6 +88,7 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
 import platform.test.runner.parameterized.Parameters
 
@@ -138,6 +142,8 @@
         )
         whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
 
+        kosmos.powerInteractor.setAwakeForTest()
+
         underTest =
             CommunalViewModel(
                 testScope,
@@ -468,6 +474,229 @@
             assertThat(isFocusable).isEqualTo(false)
         }
 
+    @Test
+    fun isCommunalContentFlowFrozen_whenActivityStartedWhileDreaming() =
+        testScope.runTest {
+            val isCommunalContentFlowFrozen by
+                collectLastValue(underTest.isCommunalContentFlowFrozen)
+
+            // 1. When dreaming not dozing
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            keyguardRepository.setDreaming(true)
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceTimeBy(60L)
+            // And keyguard is occluded by dream
+            keyguardRepository.setKeyguardOccluded(true)
+
+            // And on hub
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+
+            // Then flow is not frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
+
+            // 2. When dreaming stopped by the new activity about to show on lock screen
+            keyguardRepository.setDreamingWithOverlay(false)
+            advanceTimeBy(60L)
+
+            // Then flow is frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(true)
+
+            // 3. When transitioned to OCCLUDED and activity shows
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                testScope = testScope,
+            )
+
+            // Then flow is not frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
+        }
+
+    @Test
+    fun isCommunalContentFlowFrozen_whenActivityStartedInHandheldMode() =
+        testScope.runTest {
+            val isCommunalContentFlowFrozen by
+                collectLastValue(underTest.isCommunalContentFlowFrozen)
+
+            // 1. When on keyguard and not occluded
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setKeyguardOccluded(false)
+
+            // And transitioned to hub
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+
+            // Then flow is not frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
+
+            // 2. When occluded by a new activity
+            keyguardRepository.setKeyguardOccluded(true)
+            runCurrent()
+
+            // And transitioning to occluded
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.GLANCEABLE_HUB,
+                    to = KeyguardState.OCCLUDED,
+                    transitionState = TransitionState.STARTED,
+                )
+            )
+
+            keyguardTransitionRepository.sendTransitionStep(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                transitionState = TransitionState.RUNNING,
+                value = 0.5f,
+            )
+
+            // Then flow is frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(true)
+
+            // 3. When transition is finished
+            keyguardTransitionRepository.sendTransitionStep(
+                from = KeyguardState.GLANCEABLE_HUB,
+                to = KeyguardState.OCCLUDED,
+                transitionState = TransitionState.FINISHED,
+                value = 1f,
+            )
+
+            // Then flow is not frozen
+            assertThat(isCommunalContentFlowFrozen).isEqualTo(false)
+        }
+
+    @Test
+    fun communalContent_emitsFrozenContent_whenFrozen() =
+        testScope.runTest {
+            val communalContent by collectLastValue(underTest.communalContent)
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+            // When dreaming
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            keyguardRepository.setDreaming(true)
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceTimeBy(60L)
+            keyguardRepository.setKeyguardOccluded(true)
+
+            // And transitioned to hub
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+
+            // Widgets available
+            val widgets =
+                listOf(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 0,
+                        priority = 30,
+                        providerInfo = providerInfo,
+                    ),
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        priority = 20,
+                        providerInfo = providerInfo,
+                    ),
+                )
+            widgetRepository.setCommunalWidgets(widgets)
+
+            // Then hub shows widgets and the CTA tile
+            assertThat(communalContent).hasSize(3)
+
+            // When dreaming stopped by another activity which should freeze flow
+            keyguardRepository.setDreamingWithOverlay(false)
+            advanceTimeBy(60L)
+
+            // New timer available
+            val target = Mockito.mock(SmartspaceTarget::class.java)
+            whenever<String?>(target.smartspaceTargetId).thenReturn("target")
+            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
+            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            runCurrent()
+
+            // Still only emits widgets and the CTA tile
+            assertThat(communalContent).hasSize(3)
+            assertThat(communalContent?.get(0))
+                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
+            assertThat(communalContent?.get(1))
+                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
+            assertThat(communalContent?.get(2))
+                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
+        }
+
+    @Test
+    fun communalContent_emitsLatestContent_whenNotFrozen() =
+        testScope.runTest {
+            val communalContent by collectLastValue(underTest.communalContent)
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+            // When dreaming
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
+            keyguardRepository.setDreaming(true)
+            keyguardRepository.setDreamingWithOverlay(true)
+            advanceTimeBy(60L)
+            keyguardRepository.setKeyguardOccluded(true)
+
+            // Transitioned to Glanceable hub.
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+
+            // And widgets available
+            val widgets =
+                listOf(
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 0,
+                        priority = 30,
+                        providerInfo = providerInfo,
+                    ),
+                    CommunalWidgetContentModel.Available(
+                        appWidgetId = 1,
+                        priority = 20,
+                        providerInfo = providerInfo,
+                    ),
+                )
+            widgetRepository.setCommunalWidgets(widgets)
+
+            // Then emits widgets and the CTA tile
+            assertThat(communalContent).hasSize(3)
+
+            // When new timer available
+            val target = Mockito.mock(SmartspaceTarget::class.java)
+            whenever(target.smartspaceTargetId).thenReturn("target")
+            whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+            whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
+            smartspaceRepository.setCommunalSmartspaceTargets(listOf(target))
+            runCurrent()
+
+            // Then emits timer, widgets and the CTA tile
+            assertThat(communalContent).hasSize(4)
+            assertThat(communalContent?.get(0))
+                .isInstanceOf(CommunalContentModel.Smartspace::class.java)
+            assertThat(communalContent?.get(1))
+                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
+            assertThat(communalContent?.get(2))
+                .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
+            assertThat(communalContent?.get(3))
+                .isInstanceOf(CommunalContentModel.CtaTileInViewMode::class.java)
+        }
+
     private suspend fun setIsMainUser(isMainUser: Boolean) {
         whenever(user.isMain).thenReturn(isMainUser)
         userRepository.setUserInfos(listOf(user))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
index 723f6a2..9300db9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt
@@ -16,29 +16,40 @@
 package com.android.systemui.dreams.homecontrols
 
 import android.app.Activity
+import android.content.Intent
+import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL
+import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM
+import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE
+import android.window.TaskFragmentInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.controls.settings.FakeControlsSettingsRepository
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.wakelock.WakeLockFake
 import com.google.common.truth.Truth.assertThat
 import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+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
+import org.mockito.kotlin.whenever
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class HomeControlsDreamServiceTest : SysuiTestCase() {
@@ -46,31 +57,38 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private lateinit var fakeWakeLockBuilder: WakeLockFake.Builder
-    private lateinit var fakeWakeLock: WakeLockFake
+    private val fakeWakeLock = WakeLockFake()
+    private val fakeWakeLockBuilder by lazy {
+        WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) }
+    }
 
-    @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory
-    @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent
-    @Mock private lateinit var activity: Activity
+    private val taskFragmentComponent = mock<TaskFragmentComponent>()
+    private val activity = mock<Activity>()
+    private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
+    private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>()
+    private val hideCallback = argumentCaptor<() -> Unit>()
+    private val dreamServiceDelegate =
+        mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity }
 
-    private lateinit var underTest: HomeControlsDreamService
+    private val taskFragmentComponentFactory =
+        mock<TaskFragmentComponent.Factory> {
+            on {
+                create(
+                    activity = eq(activity),
+                    onCreateCallback = onCreateCallback.capture(),
+                    onInfoChangedCallback = onInfoChangedCallback.capture(),
+                    hide = hideCallback.capture(),
+                )
+            } doReturn taskFragmentComponent
+        }
+
+    private val underTest: HomeControlsDreamService by lazy { buildService() }
 
     @Before
-    fun setup() =
-        with(kosmos) {
-            MockitoAnnotations.initMocks(this@HomeControlsDreamServiceTest)
-            whenever(taskFragmentComponentFactory.create(any(), any(), any(), any()))
-                .thenReturn(taskFragmentComponent)
-
-            fakeWakeLock = WakeLockFake()
-            fakeWakeLockBuilder = WakeLockFake.Builder(context)
-            fakeWakeLockBuilder.setWakeLock(fakeWakeLock)
-
-            whenever(controlsComponent.getControlsListingController())
-                .thenReturn(Optional.of(controlsListingController))
-
-            underTest = buildService { activity }
-        }
+    fun setup() {
+        whenever(kosmos.controlsComponent.getControlsListingController())
+            .thenReturn(Optional.of(kosmos.controlsListingController))
+    }
 
     @Test
     fun testOnAttachedToWindowCreatesTaskFragmentComponent() =
@@ -90,9 +108,12 @@
     @Test
     fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() =
         testScope.runTest {
-            underTest = buildService { null }
+            val serviceWithNullActivity =
+                buildService(
+                    mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null }
+                )
 
-            underTest.onAttachedToWindow()
+            serviceWithNullActivity.onAttachedToWindow()
             verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any())
         }
 
@@ -102,6 +123,7 @@
             underTest.onAttachedToWindow()
             assertThat(fakeWakeLock.isHeld).isTrue()
         }
+
     @Test
     fun testDetachWindow_wakeLockCanBeReleased() =
         testScope.runTest {
@@ -112,14 +134,60 @@
             assertThat(fakeWakeLock.isHeld).isFalse()
         }
 
-    private fun buildService(activityProvider: DreamActivityProvider): HomeControlsDreamService =
+    @Test
+    fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() =
+        testScope.runTest {
+            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false)
+            underTest.onAttachedToWindow()
+            onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
+            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
+
+            // Task fragment becomes empty
+            onInfoChangedCallback.firstValue.invoke(
+                mock<TaskFragmentInfo> { on { isEmpty } doReturn true }
+            )
+            advanceUntilIdle()
+            // Dream is finished and activity is not restarted
+            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
+            verify(dreamServiceDelegate, never()).wakeUp(any())
+            verify(dreamServiceDelegate).finish(any())
+        }
+
+    @Test
+    fun testRestartsActivityWhenRedirectingWakes() =
+        testScope.runTest {
+            whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true)
+            underTest.onAttachedToWindow()
+            onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>())
+            verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher())
+
+            // Task fragment becomes empty
+            onInfoChangedCallback.firstValue.invoke(
+                mock<TaskFragmentInfo> { on { isEmpty } doReturn true }
+            )
+            advanceUntilIdle()
+            // Activity is restarted instead of finishing the dream.
+            verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher())
+            verify(dreamServiceDelegate).wakeUp(any())
+            verify(dreamServiceDelegate, never()).finish(any())
+        }
+
+    private fun intentMatcher() =
+        argThat<Intent> {
+            getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) ==
+                CONTROLS_SURFACE_DREAM
+        }
+
+    private fun buildService(
+        activityProvider: DreamServiceDelegate = dreamServiceDelegate
+    ): HomeControlsDreamService =
         with(kosmos) {
             return HomeControlsDreamService(
                 controlsSettingsRepository = FakeControlsSettingsRepository(),
                 taskFragmentFactory = taskFragmentComponentFactory,
                 homeControlsComponentInteractor = homeControlsComponentInteractor,
-                fakeWakeLockBuilder,
-                dreamActivityProvider = activityProvider,
+                wakeLockBuilder = fakeWakeLockBuilder,
+                dreamServiceDelegate = activityProvider,
                 bgDispatcher = testDispatcher,
                 logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest")
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
index c51413a..4849e66 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.qsTileFactory
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.TestScope
@@ -41,6 +42,7 @@
 
     private val kosmos = testKosmos()
     private val vibratorHelper = kosmos.vibratorHelper
+    private val qsTile = kosmos.qsTileFactory.createTile("Test Tile")
 
     private val effectDuration = 400
     private val lowTickDuration = 12
@@ -61,6 +63,7 @@
                 vibratorHelper,
                 kosmos.keyguardInteractor,
             )
+        longPressEffect.qsTile = qsTile
     }
 
     @Test
@@ -91,8 +94,10 @@
         // GIVEN an action down event occurs
         longPressEffect.handleActionDown()
 
-        // THEN the effect moves to the TIMEOUT_WAIT state
+        // THEN the effect moves to the TIMEOUT_WAIT state and starts the wait
+        val action by collectLastValue(longPressEffect.actionType)
         assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+        assertThat(action).isEqualTo(QSLongPressEffect.ActionType.WAIT_TAP_TIMEOUT)
     }
 
     @Test
@@ -107,20 +112,6 @@
         }
 
     @Test
-    fun onActionUp_whileWaiting_performsClick() =
-        testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
-            // GIVEN an action is being collected
-            val action by collectLastValue(longPressEffect.actionType)
-
-            // GIVEN an action up occurs
-            longPressEffect.handleActionUp()
-
-            // THEN the action to invoke is the click action and the effect does not start
-            assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK)
-            assertEffectDidNotStart()
-        }
-
-    @Test
     fun onWaitComplete_whileWaiting_beginsEffect() =
         testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
             // GIVEN the pressed timeout is complete
@@ -221,8 +212,10 @@
             // GIVEN that the animator was cancelled
             longPressEffect.handleAnimationCancel()
 
-            // THEN the state goes to the timeout wait
+            // THEN the state goes to the timeout wait and the wait is posted
+            val action by collectLastValue(longPressEffect.actionType)
             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
+            assertThat(action).isEqualTo(QSLongPressEffect.ActionType.WAIT_TAP_TIMEOUT)
         }
 
     @Test
@@ -238,6 +231,29 @@
             assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
         }
 
+    @Test
+    fun onTileClick_whileWaiting_withQSTile_clicks() =
+        testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
+            // GIVEN that a click was detected
+            val couldClick = longPressEffect.onTileClick()
+
+            // THEN the click is successful
+            assertThat(couldClick).isTrue()
+        }
+
+    @Test
+    fun onTileClick_whileWaiting_withoutQSTile_cannotClick() =
+        testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) {
+            // GIVEN that no QSTile has been set
+            longPressEffect.qsTile = null
+
+            // GIVEN that a click was detected
+            val couldClick = longPressEffect.onTileClick()
+
+            // THEN the click is not successful
+            assertThat(couldClick).isFalse()
+        }
+
     private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) =
         with(kosmos) {
             testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index d630a2f..6c5001a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -136,20 +136,20 @@
 
     @Test
     @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
-    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissableKeyguard() =
+    fun testTransitionToOccluded_onWakeUp_ifPowerButtonGestureDetected_fromAod_nonDismissibleKeyguard() =
         testScope.runTest {
             powerInteractor.onCameraLaunchGestureDetected()
             powerInteractor.setAwakeForTest()
             advanceTimeBy(100) // account for debouncing
 
-            // We should head back to GONE since we started there.
+            // We should head to OCCLUDED because keyguard is not dismissible.
             assertThat(transitionRepository)
                 .startedTransition(from = KeyguardState.AOD, to = KeyguardState.OCCLUDED)
         }
 
     @Test
     @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
-    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissableKeyguard() =
+    fun testTransitionToGone_onWakeUp_ifPowerButtonGestureDetected_fromAod_dismissibleKeyguard() =
         testScope.runTest {
             kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
             powerInteractor.onCameraLaunchGestureDetected()
@@ -188,6 +188,7 @@
             )
 
             // Detect a power gesture and then wake up.
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
             reset(transitionRepository)
             powerInteractor.onCameraLaunchGestureDetected()
             powerInteractor.setAwakeForTest()
@@ -355,6 +356,7 @@
             )
 
             // Detect a power gesture and then wake up.
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
             reset(transitionRepository)
             powerInteractor.onCameraLaunchGestureDetected()
             powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index bfc7775..612f2e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -56,13 +56,13 @@
 import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.testKosmos
 import junit.framework.Assert.assertEquals
-import kotlin.test.Test
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
@@ -230,6 +230,7 @@
             )
 
             // Detect a power gesture and then wake up.
+            kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
             reset(transitionRepository)
             powerInteractor.onCameraLaunchGestureDetected()
             powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 78a1167..5068f68 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -350,7 +350,7 @@
         }
 
     @Test
-    fun quickAffordance_updateOncePerShadeExpansion() =
+    fun quickAffordance_doNotSendUpdatesWhileShadeExpandingAndStillHidden() =
         testScope.runTest {
             val shadeExpansion = MutableStateFlow(0f)
             whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion)
@@ -365,7 +365,9 @@
                 shadeExpansion.value = i / 10f
             }
 
-            assertThat(collectedValue.size).isEqualTo(initialSize + 1)
+            assertThat(collectedValue[0])
+                .isInstanceOf(KeyguardQuickAffordanceModel.Hidden::class.java)
+            assertThat(collectedValue.size).isEqualTo(initialSize)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
index 04c270d..ad24a71 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModelTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.kosmos.testScope
@@ -104,6 +105,7 @@
                 burnInInteractor = burnInInteractor,
                 shortcutsCombinedViewModel = shortcutsCombinedViewModel,
                 configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
+                keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
index 37d4721..7ebebd7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt
@@ -203,6 +203,21 @@
                 .containsExactlyElementsIn(DEFAULT_TILES.toTileSpecs() + startingTiles)
         }
 
+    @Test
+    fun prependDefault_noChangesWhenInRetail() =
+        testScope.runTest {
+            val user = 0
+            retailModeRepository.setRetailMode(true)
+            val startingTiles = "a"
+            storeTilesForUser(startingTiles, user)
+
+            runCurrent()
+            underTest.prependDefault(user)
+            runCurrent()
+
+            assertThat(loadTilesForUser(user)).isEqualTo(startingTiles)
+        }
+
     private fun TestScope.storeTilesForUser(specs: String, forUser: Int) {
         secureSettings.putStringForUser(SETTING, specs, forUser)
         runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
index 58fc109..b12fbc2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepositoryTest.kt
@@ -327,6 +327,32 @@
             assertThat(loadTiles()).isEqualTo(expected)
         }
 
+    @Test
+    fun setTilesWithRepeats_onlyDistinctTiles() =
+        testScope.runTest {
+            val tilesToSet = "a,b,c,a,d,b".toTileSpecs()
+            val expected = "a,b,c,d"
+
+            val tiles by collectLastValue(underTest.tiles())
+            underTest.setTiles(tilesToSet)
+
+            assertThat(tiles).isEqualTo(expected.toTileSpecs())
+            assertThat(loadTiles()).isEqualTo(expected)
+        }
+
+    @Test
+    fun prependDefaultTwice_doesntAddMoreTiles() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles())
+            underTest.setTiles(listOf(TileSpec.create("a")))
+
+            underTest.prependDefault()
+            val currentTiles = tiles!!
+            underTest.prependDefault()
+
+            assertThat(tiles).isEqualTo(currentTiles)
+        }
+
     private fun getDefaultTileSpecs(): List<TileSpec> {
         return defaultTilesRepository.defaultTiles
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 1c73fe2b..6ad4b31 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -49,6 +49,7 @@
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.qs.tiles.di.NewQSTileFactory
 import com.android.systemui.qs.toProto
+import com.android.systemui.retail.data.repository.FakeRetailModeRepository
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.util.mockito.any
@@ -85,6 +86,7 @@
     private val pipelineFlags = QSPipelineFlagsRepository()
     private val tileLifecycleManagerFactory = TLMFactory()
     private val minimumTilesRepository = MinimumTilesFixedRepository()
+    private val retailModeRepository = FakeRetailModeRepository()
 
     @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
 
@@ -118,6 +120,7 @@
                 installedTilesComponentRepository = installedTilesPackageRepository,
                 userRepository = userRepository,
                 minimumTilesRepository = minimumTilesRepository,
+                retailModeRepository = retailModeRepository,
                 customTileStatePersister = customTileStatePersister,
                 tileFactory = tileFactory,
                 newQSTileFactory = { newQSTileFactory },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
index 260189d..e8ad038 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
@@ -34,6 +34,8 @@
 import com.android.systemui.qs.pipeline.data.repository.fakeDefaultTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeRetailModeRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.qsTileFactory
 import com.android.systemui.settings.fakeUserTracker
@@ -138,6 +140,19 @@
             }
         }
 
+    @Test
+    fun inRetailMode_onlyOneTile_noPrependDefault() =
+        with(kosmos) {
+            testScope.runTest {
+                fakeRetailModeRepository.setRetailMode(true)
+                fakeTileSpecRepository.setTiles(0, listOf(goodTile))
+                val tiles by collectLastValue(currentTilesInteractor.currentTiles)
+                runCurrent()
+
+                assertThat(tiles!!.map { it.spec }).isEqualTo(listOf(goodTile))
+            }
+        }
+
     private fun tileCreator(spec: String): QSTile? {
         return if (spec.contains("OEM")) {
             null // We don't know how to create OEM spec tiles
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index 02993b8..523a89a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -99,14 +99,14 @@
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Executor;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4.class)
 public class NotificationLockscreenUserManagerTest extends SysuiTestCase {
@@ -162,6 +162,7 @@
     private NotificationEntry mCurrentUserNotif;
     private NotificationEntry mSecondaryUserNotif;
     private NotificationEntry mWorkProfileNotif;
+    private NotificationEntry mSensitiveContentNotif;
     private final FakeFeatureFlagsClassic mFakeFeatureFlags = new FakeFeatureFlagsClassic();
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mBackgroundExecutor = new FakeExecutor(mFakeSystemClock);
@@ -224,6 +225,14 @@
         mWorkProfileNotif.setRanking(new RankingBuilder(mWorkProfileNotif.getRanking())
                 .setChannel(channel)
                 .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+        mSensitiveContentNotif = new NotificationEntryBuilder()
+                .setNotification(notifWithPrivateVisibility)
+                .setUser(new UserHandle(mCurrentUser.id))
+                .build();
+        mSensitiveContentNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
+                .setChannel(channel)
+                .setSensitiveContent(true)
+                .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
         when(mNotifCollection.getEntry(mWorkProfileNotif.getKey())).thenReturn(mWorkProfileNotif);
 
         mLockscreenUserManager = new TestNotificationLockscreenUserManager(mContext);
@@ -459,6 +468,17 @@
     }
 
     @Test
+    public void testHasSensitiveContent_redacted() {
+        // Allow private notifications for this user
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+
+        // Sensitive Content notifications are always redacted
+        assertTrue(mLockscreenUserManager.needsRedaction(mSensitiveContentNotif));
+    }
+
+    @Test
     public void testUserSwitchedCallsOnUserSwitching() {
         mLockscreenUserManager.getUserTrackerCallbackForTest().onUserChanging(mSecondaryUser.id,
                 mContext);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 82e2bb7..c35c165 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -41,6 +41,8 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -53,6 +55,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.mockLargeScreenHeaderHelper
 import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
@@ -794,11 +797,47 @@
     @DisableSceneContainer
     fun updateBounds_fromKeyguardRoot() =
         testScope.runTest {
-            val bounds by collectLastValue(underTest.bounds)
+            val startProgress = 0f
+            val startStep = TransitionStep(LOCKSCREEN, AOD, startProgress, TransitionState.STARTED)
+            val boundsChangingProgress = 0.2f
+            val boundsChangingStep =
+                TransitionStep(LOCKSCREEN, AOD, boundsChangingProgress, TransitionState.RUNNING)
+            val boundsInterpolatingProgress = 0.6f
+            val boundsInterpolatingStep =
+                TransitionStep(
+                    LOCKSCREEN,
+                    AOD,
+                    boundsInterpolatingProgress,
+                    TransitionState.RUNNING
+                )
+            val finishProgress = 1.0f
+            val finishStep =
+                TransitionStep(LOCKSCREEN, AOD, finishProgress, TransitionState.FINISHED)
 
+            val bounds by collectLastValue(underTest.bounds)
             val top = 123f
             val bottom = 456f
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(startStep)
+            runCurrent()
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(boundsChangingStep)
+            runCurrent()
             keyguardRootViewModel.onNotificationContainerBoundsChanged(top, bottom)
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(boundsInterpolatingStep)
+            runCurrent()
+            val adjustedProgress =
+                (boundsInterpolatingProgress - boundsChangingProgress) /
+                    (1 - boundsChangingProgress)
+            val interpolatedTop = interpolate(0f, top, adjustedProgress)
+            val interpolatedBottom = interpolate(0f, bottom, adjustedProgress)
+            assertThat(bounds)
+                .isEqualTo(
+                    NotificationContainerBounds(top = interpolatedTop, bottom = interpolatedBottom)
+                )
+
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(finishStep)
+            runCurrent()
             assertThat(bounds).isEqualTo(NotificationContainerBounds(top = top, bottom = bottom))
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 63f19fb..6b5d072 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -78,7 +78,7 @@
 
         // Initialize AvalancheController and TestableHeadsUpManager during setUp instead of
         // declaration, where mocks are null
-        mAvalancheController = AvalancheController(dumpManager)
+        mAvalancheController = AvalancheController(dumpManager, mUiEventLoggerFake)
 
         testableHeadsUpManager =
             TestableHeadsUpManager(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
index 3bfc046..88bef91 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java
@@ -38,6 +38,7 @@
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Person;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper;
@@ -147,7 +148,7 @@
     @Override
     public void SysuiSetup() throws Exception {
         super.SysuiSetup();
-        mAvalancheController = new AvalancheController(dumpManager);
+        mAvalancheController = new AvalancheController(dumpManager, mUiEventLoggerFake);
     }
 
     @Test
@@ -610,7 +611,31 @@
     }
 
     @Test
-    public void testPinEntry_logsPeek() {
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    public void testPinEntry_logsPeek_throttleEnabled() {
+        final BaseHeadsUpManager hum = createHeadsUpManager();
+
+        // Needs full screen intent in order to be pinned
+        final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
+                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
+
+        // Note: the standard way to show a notification would be calling showNotification rather
+        // than onAlertEntryAdded. However, in practice showNotification in effect adds
+        // the notification and then updates it; in order to not log twice, the entry needs
+        // to have a functional ExpandableNotificationRow that can keep track of whether it's
+        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+        hum.onEntryAdded(entryToPin);
+
+        assertEquals(2, mUiEventLoggerFake.numLogs());
+        assertEquals(AvalancheController.ThrottleEvent.SHOWN.getId(),
+                mUiEventLoggerFake.eventId(0));
+        assertEquals(BaseHeadsUpManager.NotificationPeekEvent.NOTIFICATION_PEEK.getId(),
+                mUiEventLoggerFake.eventId(1));
+    }
+
+    @Test
+    @DisableFlags(NotificationThrottleHun.FLAG_NAME)
+    public void testPinEntry_logsPeek_throttleDisabled() {
         final BaseHeadsUpManager hum = createHeadsUpManager();
 
         // Needs full screen intent in order to be pinned
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
index 9feb914..200e92e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.java
@@ -167,7 +167,7 @@
         mContext.getOrCreateTestableResources().addOverride(
                 R.integer.ambient_notification_extension_time, 500);
 
-        mAvalancheController = new AvalancheController(dumpManager);
+        mAvalancheController = new AvalancheController(dumpManager, mUiEventLogger);
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
index b83b93b..10a4eb7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt
@@ -50,6 +50,8 @@
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.whenever
 
+private const val builtInDeviceName = "This phone"
+
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -71,6 +73,10 @@
                 addOverride(R.drawable.ic_media_speaker_device, testIcon)
 
                 addOverride(com.android.internal.R.drawable.ic_bt_hearing_aid, testIcon)
+
+                addOverride(R.string.media_transfer_this_device_name_tv, builtInDeviceName)
+                addOverride(R.string.media_transfer_this_device_name_tablet, builtInDeviceName)
+                addOverride(R.string.media_transfer_this_device_name, builtInDeviceName)
             }
         }
     }
@@ -90,7 +96,7 @@
 
                 assertThat(device).isInstanceOf(AudioOutputDevice.BuiltIn::class.java)
                 assertThat(device!!.icon).isEqualTo(testIcon)
-                assertThat(device!!.name).isEqualTo("built_in")
+                assertThat(device!!.name).isEqualTo(builtInDeviceName)
             }
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
new file mode 100644
index 0000000..8921a23
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
+
+import android.graphics.drawable.TestStubDrawable
+import android.media.AudioManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.TestAudioDevicesFactory
+import com.android.systemui.volume.data.repository.audioRepository
+import com.android.systemui.volume.data.repository.audioSharingRepository
+import com.android.systemui.volume.domain.model.AudioOutputDevice
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.localMediaRepository
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaOutputComponentModel
+import com.android.systemui.volume.panel.shared.model.filterData
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val builtInDeviceName = "This phone"
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class MediaOutputComponentInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private lateinit var underTest: MediaOutputComponentInteractor
+
+    @Before
+    fun setUp() =
+        with(kosmos) {
+            audioRepository.setMode(AudioManager.MODE_NORMAL)
+            localMediaRepository.updateCurrentConnectedDevice(
+                TestMediaDevicesFactory.builtInMediaDevice(deviceIcon = testIcon)
+            )
+
+            with(context.orCreateTestableResources) {
+                addOverride(R.drawable.ic_smartphone, testIcon)
+
+                addOverride(R.string.media_transfer_this_device_name_tv, builtInDeviceName)
+                addOverride(R.string.media_transfer_this_device_name_tablet, builtInDeviceName)
+                addOverride(R.string.media_transfer_this_device_name, builtInDeviceName)
+            }
+
+            underTest = mediaOutputComponentInteractor
+        }
+
+    @Test
+    fun inCall_stateIs_Calling() =
+        with(kosmos) {
+            testScope.runTest {
+                with(audioRepository) {
+                    setMode(AudioManager.MODE_IN_CALL)
+                    setCommunicationDevice(TestAudioDevicesFactory.builtInDevice())
+                }
+
+                val model by collectLastValue(underTest.mediaOutputModel.filterData())
+                runCurrent()
+
+                assertThat(model)
+                    .isEqualTo(
+                        MediaOutputComponentModel.Calling(
+                            AudioOutputDevice.BuiltIn(builtInDeviceName, testIcon),
+                            false,
+                        )
+                    )
+            }
+        }
+
+    @Test
+    fun hasSession_stateIs_MediaSession() =
+        with(kosmos) {
+            testScope.runTest {
+                mediaControllerRepository.setActiveSessions(listOf(localMediaController))
+
+                val model by collectLastValue(underTest.mediaOutputModel.filterData())
+                runCurrent()
+
+                with(model as MediaOutputComponentModel.MediaSession) {
+                    assertThat(session.appLabel).isEqualTo("local_media_controller_label")
+                    assertThat(session.packageName).isEqualTo("local.test.pkg")
+                    assertThat(session.canAdjustVolume).isTrue()
+                    assertThat(device)
+                        .isEqualTo(AudioOutputDevice.BuiltIn("built_in_media", testIcon))
+                    assertThat(isInAudioSharing).isFalse()
+                }
+            }
+        }
+
+    @Test
+    fun noMediaOrCall_stateIs_Idle() =
+        with(kosmos) {
+            testScope.runTest {
+                audioSharingRepository.setInAudioSharing(true)
+
+                val model by collectLastValue(underTest.mediaOutputModel.filterData())
+                runCurrent()
+
+                assertThat(model)
+                    .isEqualTo(
+                        MediaOutputComponentModel.Idle(
+                            AudioOutputDevice.BuiltIn("built_in_media", testIcon),
+                            true,
+                        )
+                    )
+            }
+        }
+
+    private companion object {
+        val testIcon = TestStubDrawable()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index d497b4a..86a20dc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -31,14 +31,11 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.volume.data.repository.audioSharingRepository
-import com.android.systemui.volume.domain.interactor.audioModeInteractor
-import com.android.systemui.volume.domain.interactor.audioOutputInteractor
 import com.android.systemui.volume.localMediaController
 import com.android.systemui.volume.localMediaRepository
 import com.android.systemui.volume.mediaControllerRepository
-import com.android.systemui.volume.mediaDeviceSessionInteractor
 import com.android.systemui.volume.mediaOutputActionsInteractor
-import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaOutputComponentInteractor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
@@ -66,10 +63,7 @@
                     applicationContext,
                     testScope.backgroundScope,
                     mediaOutputActionsInteractor,
-                    mediaDeviceSessionInteractor,
-                    audioOutputInteractor,
-                    audioModeInteractor,
-                    mediaOutputInteractor,
+                    mediaOutputComponentInteractor,
                     uiEventLogger,
                 )
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt
new file mode 100644
index 0000000..7934b02
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorTest.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.ui.navigation
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+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.volume.domain.model.VolumePanelRoute
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModelFactory
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumeNavigatorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private val underTest: VolumeNavigator =
+        with(kosmos) {
+            VolumeNavigator(
+                testScope.backgroundScope,
+                testDispatcher,
+                mock {},
+                activityStarter,
+                volumePanelViewModelFactory,
+                mock {
+                    on { create(any(), anyInt(), anyBoolean(), any()) }.thenReturn(mock {})
+                    on { applicationContext }.thenReturn(context)
+                },
+                uiEventLoggerFake,
+                volumePanelGlobalStateInteractor,
+            )
+        }
+
+    @Test
+    fun showNewVolumePanel_keyguardLocked_notShown() =
+        with(kosmos) {
+            testScope.runTest {
+                val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState)
+
+                underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL)
+                runCurrent()
+
+                assertThat(panelState!!.isVisible).isFalse()
+            }
+        }
+
+    @Test
+    fun showNewVolumePanel_keyguardUnlocked_shown() =
+        with(kosmos) {
+            testScope.runTest {
+                whenever(activityStarter.dismissKeyguardThenExecute(any(), any(), anyBoolean()))
+                    .then { (it.arguments[0] as ActivityStarter.OnDismissAction).onDismiss() }
+                val panelState by collectLastValue(volumePanelGlobalStateInteractor.globalState)
+
+                underTest.openVolumePanel(VolumePanelRoute.COMPOSE_VOLUME_PANEL)
+                runCurrent()
+
+                assertThat(panelState!!.isVisible).isTrue()
+            }
+        }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
index 3244eb4..bf58eee 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
@@ -94,6 +94,9 @@
     default void setHasNotifications(boolean hasNotifications) {
     }
 
+    /** Sets whether the squishiness fraction should be updated on the media host. */
+    default void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {}
+
     /**
      * Should touches from the notification panel be disallowed?
      * The notification panel might grab any touches rom QS at any time to collapse the shade.
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml b/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml
new file mode 100644
index 0000000..82b222e7
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M360,880q-134,0 -227,-93T40,560h80q0,100 70,170t170,70v80ZM360,740q-75,0 -127.5,-52.5T180,560h80q0,42 29,71t71,29v80ZM400,600q-33,0 -56.5,-23.5T320,520v-320q0,-33 23.5,-56.5T400,120h160q33,0 56.5,23.5T640,200v320q0,33 -23.5,56.5T560,600L400,600ZM400,520h160v-320L400,200v320ZM600,740v-80q42,0 71,-29t29,-71h80q0,75 -52.5,127.5T600,740ZM600,880v-80q100,0 170,-70t70,-170h80q0,134 -93,227T600,880ZM400,520h160,-160Z" />
+</vector>
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
deleted file mode 100644
index 4a2a1cb..0000000
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-** Copyright 2022, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
--->
-
-<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/footer_actions_height"
-    android:elevation="@dimen/qs_panel_elevation"
-    android:paddingTop="@dimen/qs_footer_actions_top_padding"
-    android:paddingBottom="@dimen/qs_footer_actions_bottom_padding"
-    android:background="@drawable/qs_footer_actions_background"
-    android:gravity="center_vertical|end"
-    android:layout_gravity="bottom"
-/>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml
deleted file mode 100644
index fad41c82..0000000
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_icon_button.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/qs_footer_action_button_size"
-    android:layout_height="@dimen/qs_footer_action_button_size"
-    android:visibility="gone">
-    <ImageView
-        android:id="@+id/icon"
-        android:layout_width="@dimen/qs_footer_icon_size"
-        android:layout_height="@dimen/qs_footer_icon_size"
-        android:layout_gravity="center"
-        android:scaleType="centerInside" />
-</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml
deleted file mode 100644
index c09607d..0000000
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_number_button.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/qs_footer_action_button_size"
-    android:layout_height="@dimen/qs_footer_action_button_size"
-    android:background="@drawable/qs_footer_action_circle"
-    android:visibility="gone">
-    <TextView
-        android:id="@+id/number"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
-        android:layout_gravity="center"
-        android:textColor="?attr/onShadeInactiveVariant"
-        android:textSize="18sp"/>
-    <ImageView
-        android:id="@+id/new_dot"
-        android:layout_width="12dp"
-        android:layout_height="12dp"
-        android:scaleType="fitCenter"
-        android:layout_gravity="bottom|end"
-        android:src="@drawable/fgs_dot"
-        android:contentDescription="@string/fgs_dot_content_description" />
-</com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml b/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
deleted file mode 100644
index 1c31f1d..0000000
--- a/packages/SystemUI/res-keyguard/layout/footer_actions_text_button.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.systemui.animation.view.LaunchableLinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="0dp"
-    android:layout_height="@dimen/qs_security_footer_single_line_height"
-    android:layout_weight="1"
-    android:orientation="horizontal"
-    android:paddingHorizontal="@dimen/qs_footer_padding"
-    android:gravity="center_vertical"
-    android:layout_marginEnd="@dimen/qs_footer_action_inset"
-    android:background="@drawable/qs_security_footer_background"
-    android:visibility="gone">
-    <ImageView
-        android:id="@+id/icon"
-        android:layout_width="@dimen/qs_footer_icon_size"
-        android:layout_height="@dimen/qs_footer_icon_size"
-        android:gravity="start"
-        android:layout_marginEnd="12dp"
-        android:contentDescription="@null"
-        android:src="@drawable/ic_info_outline"
-        android:tint="?attr/onSurfaceVariant" />
-
-    <TextView
-        android:id="@+id/text"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:maxLines="1"
-        android:ellipsize="end"
-        android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
-        android:textColor="?attr/onSurfaceVariant"/>
-
-    <ImageView
-        android:id="@+id/new_dot"
-        android:layout_width="12dp"
-        android:layout_height="12dp"
-        android:scaleType="fitCenter"
-        android:src="@drawable/fgs_dot"
-        android:contentDescription="@string/fgs_dot_content_description"
-        />
-
-    <ImageView
-        android:id="@+id/chevron_icon"
-        android:layout_width="@dimen/qs_footer_icon_size"
-        android:layout_height="@dimen/qs_footer_icon_size"
-        android:layout_marginStart="8dp"
-        android:contentDescription="@null"
-        android:src="@*android:drawable/ic_chevron_end"
-        android:autoMirrored="true"
-        android:tint="?attr/onSurfaceVariant" />
-</com.android.systemui.animation.view.LaunchableLinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml
deleted file mode 100644
index a8abd79..0000000
--- a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_pressed="true" android:color="?attr/onShadeActive" android:alpha="0.12" />
-    <item android:state_hovered="true" android:color="?attr/onShadeActive" android:alpha="0.09" />
-    <item android:color="@color/transparent" />
-</selector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/fgs_dot.xml b/packages/SystemUI/res/drawable/fgs_dot.xml
deleted file mode 100644
index 0881d7c..0000000
--- a/packages/SystemUI/res/drawable/fgs_dot.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-** Copyright 2022, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="oval"
-    android:width="12dp"
-    android:height="12dp">
-    <solid android:color="?attr/tertiary" />
-</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
deleted file mode 100644
index 4a5d4af..0000000
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-       android:inset="@dimen/qs_footer_action_inset">
-    <ripple
-        android:color="?android:attr/colorControlHighlight">
-        <item android:id="@android:id/mask">
-            <!-- We make this shape a rounded rectangle instead of a oval so that it can animate -->
-            <!-- properly into an app/dialog. -->
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/white"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <solid android:color="?attr/shadeInactive"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-
-    </ripple>
-</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
deleted file mode 100644
index 47a2965..0000000
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="@dimen/qs_footer_action_inset">
-    <ripple
-        android:color="?android:attr/colorControlHighlight">
-        <item android:id="@android:id/mask">
-            <!-- We make this shape a rounded rectangle instead of a oval so that it can animate -->
-            <!-- properly into an app/dialog. -->
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/white"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <solid android:color="?attr/shadeActive"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <solid android:color="@color/qs_footer_power_button_overlay_color"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-
-    </ripple>
-</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_security_footer_background.xml b/packages/SystemUI/res/drawable/qs_security_footer_background.xml
deleted file mode 100644
index 0b0055b..0000000
--- a/packages/SystemUI/res/drawable/qs_security_footer_background.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetTop="@dimen/qs_footer_action_inset"
-    android:insetBottom="@dimen/qs_footer_action_inset"
-    >
-    <ripple
-        android:color="?android:attr/colorControlHighlight">
-        <item android:id="@android:id/mask">
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/white"/>
-                <corners android:radius="@dimen/qs_security_footer_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <stroke android:width="1dp"
-                        android:color="?attr/shadeInactive"/>
-                <corners android:radius="@dimen/qs_security_footer_corner_radius"/>
-            </shape>
-        </item>
-    </ripple>
-</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml b/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml
deleted file mode 100644
index 29a014a..0000000
--- a/packages/SystemUI/res/drawable/rounded_bg_full_large_radius.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <solid android:color="?androidprv:attr/colorAccentPrimary" />
-    <corners android:radius="40dp" />
-</shape>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
index 292e496..06d1bf4 100644
--- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -5,9 +5,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <LinearLayout
+    <FrameLayout
         android:id="@+id/shortcut_helper_sheet"
-        style="@style/Widget.Material3.BottomSheet"
+        style="@style/ShortcutHelperBottomSheet"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="vertical"
@@ -19,13 +19,9 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content" />
 
-        <TextView
+        <androidx.compose.ui.platform.ComposeView
+            android:id="@+id/shortcut_helper_compose_container"
             android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:textAppearance="?textAppearanceDisplayLarge"
-            android:background="?colorTertiaryContainer"
-            android:text="Shortcut Helper Content" />
-    </LinearLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
+            android:layout_height="match_parent" />
+    </FrameLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_activity.xml b/packages/SystemUI/res/layout/people_space_activity.xml
deleted file mode 100644
index f45cc7c..0000000
--- a/packages/SystemUI/res/layout/people_space_activity.xml
+++ /dev/null
@@ -1,23 +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.
-  -->
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/container"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-    <!-- The content of people_space_activity_(no|with)_conversations.xml will be added here at
-         runtime depending on the number of conversations to show. -->
-</FrameLayout>
diff --git a/packages/SystemUI/res/layout/people_space_activity_list_divider.xml b/packages/SystemUI/res/layout/people_space_activity_list_divider.xml
deleted file mode 100644
index 3b9fb3b..0000000
--- a/packages/SystemUI/res/layout/people_space_activity_list_divider.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2021 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<View
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="2dp"
-    android:background="?android:attr/colorBackground" />
diff --git a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
deleted file mode 100644
index a97c90c..0000000
--- a/packages/SystemUI/res/layout/people_space_activity_no_conversations.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-<!--
-  ~ Copyright (C) 2021 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<RelativeLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/top_level_no_conversations"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:padding="24dp"
-    android:clipToOutline="true">
-    <TextView
-        android:id="@+id/select_conversation_title"
-        android:gravity="center"
-        android:text="@string/select_conversation_title"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_centerHorizontal="true"
-        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="24sp"
-        android:layout_alignParentTop="true" />
-
-    <TextView
-        android:id="@+id/select_conversation"
-        android:gravity="center"
-        android:text="@string/no_conversations_text"
-        android:layout_width="match_parent"
-        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="16sp"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-        android:padding="24dp"
-        android:layout_marginTop="26dp"
-        android:layout_below="@id/select_conversation_title"/>
-
-    <Button
-        style="?android:attr/buttonBarButtonStyle"
-        android:id="@+id/got_it_button"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:background="@drawable/rounded_bg_full_large_radius"
-        android:text="@string/got_it"
-        android:textColor="?androidprv:attr/textColorOnAccent"
-        android:layout_marginBottom="60dp"
-        android:layout_alignParentBottom="true" />
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_above="@id/got_it_button"
-        android:layout_below="@id/select_conversation"
-        android:layout_centerInParent="true"
-        android:clipToOutline="true">
-        <LinearLayout
-            android:id="@+id/widget_initial_layout"
-            android:layout_width="200dp"
-            android:layout_height="100dp"
-            android:layout_gravity="center"
-            android:background="@drawable/rounded_bg_full_large_radius"
-            android:layout_above="@id/got_it_button">
-            <include layout="@layout/people_space_placeholder_layout" />
-        </LinearLayout>
-    </LinearLayout>
-</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml b/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
deleted file mode 100644
index 2384963..0000000
--- a/packages/SystemUI/res/layout/people_space_activity_with_conversations.xml
+++ /dev/null
@@ -1,115 +0,0 @@
-<!--
-  ~ Copyright (C) 2022 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/top_level_with_conversations"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:padding="8dp">
-    <TextView
-        android:id="@+id/select_conversation_title"
-        android:text="@string/select_conversation_title"
-        android:gravity="center"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="24sp"/>
-
-    <TextView
-        android:id="@+id/select_conversation"
-        android:text="@string/select_conversation_text"
-        android:gravity="center"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_horizontal"
-        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="16sp"
-        android:paddingVertical="24dp"
-        android:paddingHorizontal="48dp"/>
-
-    <androidx.core.widget.NestedScrollView
-        android:id="@+id/scroll_view"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <LinearLayout
-            android:id="@+id/scroll_layout"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:orientation="vertical">
-
-            <LinearLayout
-                android:id="@+id/priority"
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="35dp">
-                <TextView
-                    android:id="@+id/priority_header"
-                    android:text="@string/priority_conversations"
-                    android:layout_width="wrap_content"
-                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
-                    android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
-                    android:textSize="14sp"
-                    android:paddingStart="16dp"
-                    android:layout_height="wrap_content"/>
-
-                <LinearLayout
-                    android:id="@+id/priority_tiles"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="10dp"
-                    android:orientation="vertical"
-                    android:background="@drawable/rounded_bg_full_large_radius"
-                    android:clipToOutline="true">
-                </LinearLayout>
-            </LinearLayout>
-
-            <LinearLayout
-                android:id="@+id/recent"
-                android:orientation="vertical"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content">
-                <TextView
-                    android:id="@+id/recent_header"
-                    android:gravity="start"
-                    android:text="@string/recent_conversations"
-                    android:layout_width="wrap_content"
-                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
-                    android:textColor="?androidprv:attr/colorAccentPrimaryVariant"
-                    android:textSize="14sp"
-                    android:paddingStart="16dp"
-                    android:layout_height="wrap_content"/>
-
-                <LinearLayout
-                    android:id="@+id/recent_tiles"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="10dp"
-                    android:orientation="vertical"
-                    android:background="@drawable/rounded_bg_full_large_radius"
-                    android:clipToOutline="true">
-                </LinearLayout>
-            </LinearLayout>
-        </LinearLayout>
-    </androidx.core.widget.NestedScrollView>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/people_space_tile_view.xml b/packages/SystemUI/res/layout/people_space_tile_view.xml
deleted file mode 100644
index b0599ca..0000000
--- a/packages/SystemUI/res/layout/people_space_tile_view.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ 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.
-  -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:id="@+id/tile_view"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:orientation="vertical">
-
-    <LinearLayout
-        android:orientation="vertical"
-        android:background="?androidprv:attr/colorSurface"
-        android:padding="12dp"
-        android:elevation="4dp"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <LinearLayout
-            android:orientation="horizontal"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:gravity="start">
-
-            <ImageView
-                android:id="@+id/tile_view_person_icon"
-                android:layout_width="@dimen/avatar_size_for_medium"
-                android:layout_height="@dimen/avatar_size_for_medium" />
-
-            <LinearLayout
-                android:orientation="horizontal"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical">
-
-                <TextView
-                    android:id="@+id/tile_view_name"
-                    android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem"
-                    android:paddingHorizontal="16dp"
-                    android:textSize="22sp"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"/>
-            </LinearLayout>
-        </LinearLayout>
-    </LinearLayout>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index e3c5a7d..5f77f61 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -47,13 +47,12 @@
 
     <include layout="@layout/quick_status_bar_expanded_header" />
 
-    <include
-        layout="@layout/footer_actions"
+    <androidx.compose.ui.platform.ComposeView
         android:id="@+id/qs_footer_actions"
         android:layout_height="@dimen/footer_actions_height"
         android:layout_width="match_parent"
         android:layout_gravity="bottom"
-        />
+        android:elevation="@dimen/qs_panel_elevation" />
 
     <include
         android:id="@+id/qs_customize"
diff --git a/packages/SystemUI/res/raw/face_dialog_authenticating.json b/packages/SystemUI/res/raw/face_dialog_authenticating.json
deleted file mode 100644
index 4e25e6d..0000000
--- a/packages/SystemUI/res/raw/face_dialog_authenticating.json
+++ /dev/null
@@ -1 +0,0 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":61,"w":64,"h":64,"nm":"face_scanning 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[32,32,0],"ix":2,"l":2},"a":{"a":0,"k":[27.25,27.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[95,95,100]},{"t":60,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.244,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.244,0]],"v":[[-2.249,0.001],[0.001,2.251],[2.249,0.001],[0.001,-2.251]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[15.1,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.243],[-1.242,0],[0,1.243],[1.242,0]],"o":[[0,1.243],[1.242,0],[0,-1.243],[-1.242,0]],"v":[[-2.249,0],[0.001,2.25],[2.249,0],[0.001,-2.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[39.4,20.495],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.814,3.523],[-2.814,3.523],[-2.814,1.363],[0.652,1.363],[0.652,-3.523],[2.814,-3.523]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.791,28.479],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.154,0.15],[0,0],[0.117,-0.095],[0,0],[0.228,-0.121],[0.358,-0.103],[0.922,0.261],[0.3,0.16],[0.24,0.185],[0.14,0.139],[0.178,0.261],[0.143,0.451],[0,0],[0,0.494],[0,0],[-0.214,-0.676],[-0.392,-0.572],[-0.323,-0.317],[-0.228,-0.177],[-0.333,-0.179],[-0.503,-0.145],[-0.662,0],[-0.653,0.184],[-0.437,0.233],[-0.336,0.258],[0,0],[0,0]],"o":[[0,0],[-0.107,0.106],[0,0],[-0.24,0.185],[-0.301,0.16],[-0.92,0.261],[-0.357,-0.103],[-0.228,-0.121],[-0.158,-0.122],[-0.225,-0.221],[-0.272,-0.393],[0,0],[-0.147,-0.466],[0,0],[0,0.716],[0.206,0.656],[0.256,0.372],[0.204,0.201],[0.336,0.258],[0.436,0.233],[0.655,0.184],[0.662,0],[0.503,-0.145],[0.332,-0.179],[0,0],[0,0],[0.165,-0.136]],"v":[[6.094,1.465],[4.579,-0.076],[4.242,0.225],[4.124,0.315],[3.43,0.771],[2.439,1.165],[-0.342,1.165],[-1.331,0.771],[-2.027,0.315],[-2.48,-0.075],[-3.087,-0.801],[-3.712,-2.075],[-3.712,-2.075],[-3.934,-3.523],[-6.094,-3.523],[-5.771,-1.424],[-4.868,0.424],[-3.995,1.465],[-3.344,2.027],[-2.35,2.676],[-0.934,3.243],[1.049,3.523],[3.031,3.243],[4.449,2.676],[5.441,2.027],[5.482,1.997],[5.615,1.895]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[26.201,40.411],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-13.398,0],[0,-13.4],[13.398,0],[0,13.4]],"o":[[13.398,0],[0,13.4],[-13.398,0],[0,-13.4]],"v":[[0,-24.3],[24.3,0],[0,24.3],[-24.3,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[14.904,0],[0,-14.904],[-14.904,0],[0,14.904]],"o":[[-14.904,0],[0,14.904],[14.904,0],[0,-14.904]],"v":[[0,-27],[-27,0],[0,27],[27,0]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823529412,0.780392216701,0.980392216701,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[60]},{"t":60,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[27.25,27.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":4,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1200,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 56ebc06..aea79e8 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -28,9 +28,7 @@
 
     <!--  In landscape the security footer is actually part of the header,
     and needs to be as short as the header  -->
-    <dimen name="qs_security_footer_single_line_height">@*android:dimen/quick_qs_offset_height</dimen>
     <dimen name="qs_footer_padding">14dp</dimen>
-    <dimen name="qs_security_footer_background_inset">12dp</dimen>
 
     <dimen name="volume_tool_tip_top_margin">12dp</dimen>
     <dimen name="volume_row_slider_height">128dp</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 664fbeb..29e0dbe 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -68,11 +68,6 @@
 
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
 
-    <!--  For large screens the security footer appears below the footer,
-    same as phones in portrait  -->
-    <dimen name="qs_security_footer_single_line_height">48dp</dimen>
-    <dimen name="qs_security_footer_background_inset">0dp</dimen>
-
     <dimen name="qs_panel_padding_top">8dp</dimen>
 
     <!-- The width of large/content heavy dialogs (e.g. Internet, Media output, etc) -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 380a79e..8ce2068 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -713,7 +713,6 @@
     <dimen name="qs_header_mobile_icon_size">@dimen/status_bar_icon_drawing_size</dimen>
     <dimen name="qs_header_carrier_separator_width">6dp</dimen>
     <dimen name="qs_carrier_margin_width">4dp</dimen>
-    <dimen name="qs_footer_icon_size">20dp</dimen>
     <dimen name="qs_header_height">120dp</dimen>
     <dimen name="qs_header_row_min_height">48dp</dimen>
 
@@ -721,11 +720,7 @@
     <dimen name="new_qs_header_non_clickable_element_height">24sp</dimen>
 
     <dimen name="qs_footer_padding">20dp</dimen>
-    <dimen name="qs_security_footer_height">88dp</dimen>
-    <dimen name="qs_security_footer_single_line_height">48dp</dimen>
     <dimen name="qs_footers_margin_bottom">8dp</dimen>
-    <dimen name="qs_security_footer_background_inset">0dp</dimen>
-    <dimen name="qs_security_footer_corner_radius">28dp</dimen>
 
     <dimen name="segmented_button_spacing">0dp</dimen>
     <dimen name="borderless_button_radius">2dp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index b993a5a..177ba598 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -259,9 +259,6 @@
     <!-- ID of the Scene Container root Composable view -->
     <item type='id' name="scene_container_root_composable" />
 
-    <!-- Tag set on the Compose implementation of the QS footer actions. -->
-    <item type="id" name="tag_compose_qs_footer_actions" />
-
     <!--
     Ids for the device entry icon.
         device_entry_icon_view: parent view of both device_entry_icon and device_entry_icon_bg
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c038a82..1226bbf 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3489,6 +3489,45 @@
     <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
     <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
 
+    <!-- Title of the keyboard shortcut helper category "System". The helper is a component that
+         shows the user which keyboard shortcuts they can use. The "System" shortcuts are for
+         example "Take a screenshot" or "Go back". [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_system">System</string>
+    <!-- Title of the keyboard shortcut helper category "Multitasking". The helper is a component
+         that shows the user which keyboard shortcuts they can use. The "Multitasking" shortcuts are
+         for example "Enter split screen". [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_multitasking">Multitasking</string>
+    <!-- Title of the keyboard shortcut helper category "Input". The helper is a component
+         that shows the user which keyboard shortcuts they can use. The "Input" shortcuts are
+         the ones provided by the keyboard. Examples are "Access emoji" or "Switch to next language"
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_input">Input</string>
+    <!-- Title of the keyboard shortcut helper category "App shortcuts". The helper is a component
+         that shows the user which keyboard shortcuts they can use. The "App shortcuts" are
+         for example "Open browser" or "Open calculator". [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_app_shortcuts">App shortcuts</string>
+    <!-- Title of the keyboard shortcut helper category "Accessibility". The helper is a component
+         that shows the user which keyboard shortcuts they can use. The "Accessibility" shortcuts
+         are for example "Turn on talkback". [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_category_a11y">Accessibility</string>
+    <!-- Title at the top of the keyboard shortcut helper UI. The helper is a component
+         that shows the user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_title">Keyboard shortcuts</string>
+    <!-- Placeholder text shown in the search box of the keyboard shortcut helper, when the user
+         hasn't typed in anything in the search box yet. The helper is a  component that shows the
+         user which keyboard shortcuts they can use. [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_search_placeholder">Search shortcuts</string>
+    <!-- Content description of the icon that allows to collapse a keyboard shortcut helper category
+         panel. The helper is a  component that shows the  user which keyboard shortcuts they can
+         use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_content_description_collapse_icon">Collapse icon</string>
+    <!-- Content description of the icon that allows to expand a keyboard shortcut helper category
+         panel. The helper is a  component that shows the  user which keyboard shortcuts they can
+         use. The helper shows shortcuts in categories, which can be collapsed or expanded.
+         [CHAR LIMIT=NONE] -->
+    <string name="shortcut_helper_content_description_expand_icon">Expand icon</string>
+
     <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
     <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b8f71c1..1e0adec 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -149,11 +149,6 @@
         <item name="android:letterSpacing">0.01</item>
     </style>
 
-    <style name="TextAppearance.QS.SecurityFooter" parent="@style/TextAppearance.QS.Status">
-        <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
-        <item name="android:textColor">?attr/onSurface</item>
-    </style>
-
     <style name="TextAppearance.QS.Status.Carriers" />
 
     <style name="TextAppearance.QS.Status.Carriers.NoCarrierText">
@@ -1670,6 +1665,10 @@
         <item name="android:colorBackground">@color/transparent</item>
     </style>
 
+    <style name="ShortcutHelperBottomSheet" parent="@style/Widget.Material3.BottomSheet">
+        <item name="backgroundTint">?colorSurfaceContainer</item>
+    </style>
+
     <style name="ShortcutHelperAnimation" parent="@android:style/Animation.Activity">
         <item name="android:activityOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
         <item name="android:taskOpenEnterAnimation">@anim/shortcut_helper_launch_anim</item>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index 1366226..e8eb53f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.flags
 
 import android.app.Activity
+import android.content.pm.PackageManager
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
@@ -35,6 +36,7 @@
 ) : FlagListenable {
     companion object {
         const val RECEIVING_PACKAGE = "com.android.systemui"
+        const val RECEIVING_PACKAGE_WATCH = "com.google.android.apps.wearable.systemui"
         const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
         const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
         const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
@@ -62,7 +64,7 @@
 
     fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
         val intent = Intent(ACTION_GET_FLAGS)
-        intent.setPackage(RECEIVING_PACKAGE)
+        intent.setPackage(if (isWatch()) RECEIVING_PACKAGE_WATCH else RECEIVING_PACKAGE)
 
         return CallbackToFutureAdapter.getFuture {
                 completer: CallbackToFutureAdapter.Completer<Collection<Flag<*>>> ->
@@ -193,6 +195,10 @@
         restartAction?.accept(suppressRestart)
     }
 
+    private fun isWatch(): Boolean {
+        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+    }
+
     private data class PerFlagListener(val name: String, val listener: FlagListenable.Listener)
 }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
index 3bf3fb3..b116e29 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
@@ -308,7 +308,13 @@
             } else {
                 // Don't listen and clear out the text when the device isn't a phone.
                 mMainExecutor.execute(() -> callback.updateCarrierInfo(
-                        new CarrierTextCallbackInfo("", null, false, null)
+                        new CarrierTextCallbackInfo(
+                                /* carrierText= */ "",
+                                /* listOfCarriers= */ null,
+                                /* anySimReady= */ false,
+                                /* isInSatelliteMode= */ false,
+                                /* subscriptionIds= */ null,
+                                /* airplaneMode= */ false)
                 ));
             }
         } else {
@@ -448,10 +454,12 @@
             displayText = currentSatelliteText;
         }
 
+        boolean isInSatelliteMode = mSatelliteCarrierText != null;
         final CarrierTextCallbackInfo info = new CarrierTextCallbackInfo(
                 displayText,
                 carrierNames,
                 !allSimsMissing,
+                isInSatelliteMode,
                 subsIds,
                 airplaneMode);
         mLogger.logCallbackSentFromUpdate(info);
@@ -757,21 +765,35 @@
         public final CharSequence carrierText;
         public final CharSequence[] listOfCarriers;
         public final boolean anySimReady;
+        public final boolean isInSatelliteMode;
         public final int[] subscriptionIds;
         public boolean airplaneMode;
 
         @VisibleForTesting
-        public CarrierTextCallbackInfo(CharSequence carrierText, CharSequence[] listOfCarriers,
-                boolean anySimReady, int[] subscriptionIds) {
-            this(carrierText, listOfCarriers, anySimReady, subscriptionIds, false);
+        public CarrierTextCallbackInfo(
+                CharSequence carrierText,
+                CharSequence[] listOfCarriers,
+                boolean anySimReady,
+                int[] subscriptionIds) {
+            this(carrierText,
+                    listOfCarriers,
+                    anySimReady,
+                    /* isInSatelliteMode= */ false,
+                    subscriptionIds,
+                    /* airplaneMode= */ false);
         }
 
-        @VisibleForTesting
-        public CarrierTextCallbackInfo(CharSequence carrierText, CharSequence[] listOfCarriers,
-                boolean anySimReady, int[] subscriptionIds, boolean airplaneMode) {
+        public CarrierTextCallbackInfo(
+                CharSequence carrierText,
+                CharSequence[] listOfCarriers,
+                boolean anySimReady,
+                boolean isInSatelliteMode,
+                int[] subscriptionIds,
+                boolean airplaneMode) {
             this.carrierText = carrierText;
             this.listOfCarriers = listOfCarriers;
             this.anySimReady = anySimReady;
+            this.isInSatelliteMode = isInSatelliteMode;
             this.subscriptionIds = subscriptionIds;
             this.airplaneMode = airplaneMode;
         }
@@ -782,6 +804,7 @@
                     + "carrierText=" + carrierText
                     + ", listOfCarriers=" + Arrays.toString(listOfCarriers)
                     + ", anySimReady=" + anySimReady
+                    + ", isInSatelliteMode=" + isInSatelliteMode
                     + ", subscriptionIds=" + Arrays.toString(subscriptionIds)
                     + ", airplaneMode=" + airplaneMode
                     + '}';
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
index a74b0b0..b8ff3bb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractor.kt
@@ -98,11 +98,11 @@
         ) { unscaledSensorLocation, scale ->
             val sensorLocation =
                 SensorLocation(
-                    unscaledSensorLocation.sensorLocationX,
-                    unscaledSensorLocation.sensorLocationY,
-                    unscaledSensorLocation.sensorRadius,
+                    naturalCenterX = unscaledSensorLocation.sensorLocationX,
+                    naturalCenterY = unscaledSensorLocation.sensorLocationY,
+                    naturalRadius = unscaledSensorLocation.sensorRadius,
+                    scale = scale
                 )
-            sensorLocation.scale = scale
             sensorLocation
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
index dddadbd..2f2f3a3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorLocation.kt
@@ -16,18 +16,18 @@
 
 package com.android.systemui.biometrics.shared.model
 
-/** Provides current sensor location information in the current screen resolution [scale]. */
+/**
+ * Provides current sensor location information in the current screen resolution [scale].
+ *
+ * @property scale Scale to apply to the sensor location's natural parameters to support different
+ *   screen resolutions.
+ */
 data class SensorLocation(
     private val naturalCenterX: Int,
     private val naturalCenterY: Int,
-    private val naturalRadius: Int
+    private val naturalRadius: Int,
+    private val scale: Float = 1f
 ) {
-    /**
-     * Scale to apply to the sensor location's natural parameters to support different screen
-     * resolutions.
-     */
-    var scale: Float = 1f
-
     val centerX: Float
         get() {
             return naturalCenterX * scale
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
index fcc6992..9e836c3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
@@ -17,7 +17,9 @@
 
 package com.android.systemui.biometrics.ui.binder
 
+import android.graphics.drawable.Animatable2
 import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.airbnb.lottie.LottieAnimationView
@@ -28,8 +30,8 @@
 import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.res.R
 import com.android.systemui.util.kotlin.Utils.Companion.toQuad
+import com.android.systemui.util.kotlin.Utils.Companion.toQuint
 import com.android.systemui.util.kotlin.Utils.Companion.toTriple
 import com.android.systemui.util.kotlin.sample
 import kotlinx.coroutines.flow.combine
@@ -61,6 +63,16 @@
                 }
 
                 var faceIcon: AnimatedVectorDrawable? = null
+                val faceIconCallback =
+                    object : Animatable2.AnimationCallback() {
+                        override fun onAnimationStart(drawable: Drawable) {
+                            viewModel.onAnimationStart()
+                        }
+
+                        override fun onAnimationEnd(drawable: Drawable) {
+                            viewModel.onAnimationEnd()
+                        }
+                    }
 
                 if (!constraintBp()) {
                     launch {
@@ -126,13 +138,19 @@
                             combine(
                                 viewModel.activeAuthType,
                                 viewModel.shouldAnimateIconView,
+                                viewModel.shouldRepeatAnimation,
                                 viewModel.showingError,
-                                ::Triple
+                                ::toQuad
                             ),
-                            ::toQuad
+                            ::toQuint
                         )
-                        .collect { (iconAsset, activeAuthType, shouldAnimateIconView, showingError)
-                            ->
+                        .collect {
+                            (
+                                iconAsset,
+                                activeAuthType,
+                                shouldAnimateIconView,
+                                shouldRepeatAnimation,
+                                showingError) ->
                             if (iconAsset != -1) {
                                 when (activeAuthType) {
                                     AuthType.Fingerprint,
@@ -145,27 +163,21 @@
                                         }
                                     }
                                     AuthType.Face -> {
-                                        // TODO(b/318569643): Consolidate logic once all face auth
-                                        // assets are migrated from drawable to json
-                                        if (iconAsset == R.raw.face_dialog_authenticating) {
-                                            iconView.setAnimation(iconAsset)
-                                            iconView.frame = 0
-
+                                        faceIcon?.apply {
+                                            unregisterAnimationCallback(faceIconCallback)
+                                            stop()
+                                        }
+                                        faceIcon =
+                                            iconView.context.getDrawable(iconAsset)
+                                                as AnimatedVectorDrawable
+                                        faceIcon?.apply {
+                                            iconView.setImageDrawable(this)
                                             if (shouldAnimateIconView) {
-                                                iconView.playAnimation()
-                                                iconView.loop(true)
-                                            }
-                                        } else {
-                                            faceIcon?.apply { stop() }
-                                            faceIcon =
-                                                iconView.context.getDrawable(iconAsset)
-                                                    as AnimatedVectorDrawable
-                                            faceIcon?.apply {
-                                                iconView.setImageDrawable(this)
-                                                if (shouldAnimateIconView) {
-                                                    forceAnimationOnUI()
-                                                    start()
+                                                forceAnimationOnUI()
+                                                if (shouldRepeatAnimation) {
+                                                    registerAnimationCallback(faceIconCallback)
                                                 }
+                                                start()
                                             }
                                         }
                                     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 901d751..bde3e99 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -21,6 +21,7 @@
 import android.annotation.RawRes
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.hardware.face.Face
 import android.util.RotationUtils
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
@@ -31,10 +32,12 @@
 import com.android.systemui.util.kotlin.combine
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /**
  * Models UI of [BiometricPromptLayout.iconView] and [BiometricPromptLayout.biometric_icon_overlay]
@@ -55,8 +58,11 @@
     }
 
     /**
-     * Indicates what auth type the UI currently displays. Fingerprint-only auth -> Fingerprint
-     * Face-only auth -> Face Co-ex auth, implicit flow -> Face Co-ex auth, explicit flow -> Coex
+     * Indicates what auth type the UI currently displays.
+     * Fingerprint-only auth -> Fingerprint
+     * Face-only auth -> Face
+     * Co-ex auth, implicit flow -> Face
+     * Co-ex auth, explicit flow -> Coex
      */
     val activeAuthType: Flow<AuthType> =
         combine(
@@ -113,6 +119,35 @@
         _previousIconOverlayWasError.value = previousIconOverlayWasError
     }
 
+    /** Called when iconView begins animating. */
+    fun onAnimationStart() {
+        _animationEnded.value = false
+    }
+
+    /** Called when iconView ends animating. */
+    fun onAnimationEnd() {
+        _animationEnded.value = true
+    }
+
+    private val _animationEnded: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+    /**
+     * Whether a face iconView should pulse (i.e. while isAuthenticating and previous animation
+     * ended).
+     */
+    val shouldPulseAnimation: Flow<Boolean> =
+        combine(_animationEnded, promptViewModel.isAuthenticating) {
+                animationEnded,
+                isAuthenticating ->
+                animationEnded && isAuthenticating
+            }
+            .distinctUntilChanged()
+
+    private val _lastPulseLightToDark: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+    /** Tracks whether a face iconView last pulsed light to dark (vs. dark to light) */
+    val lastPulseLightToDark: Flow<Boolean> = _lastPulseLightToDark.asStateFlow()
+
     val iconSize: Flow<Pair<Int, Int>> =
         combine(
             promptViewModel.position,
@@ -160,22 +195,35 @@
                         }
                     }
                 AuthType.Face ->
-                    combine(
-                        promptViewModel.isAuthenticated.distinctUntilChanged(),
-                        promptViewModel.isAuthenticating.distinctUntilChanged(),
-                        promptViewModel.isPendingConfirmation.distinctUntilChanged(),
-                        promptViewModel.showingError.distinctUntilChanged()
-                    ) {
-                        authState: PromptAuthState,
-                        isAuthenticating: Boolean,
-                        isPendingConfirmation: Boolean,
-                        showingError: Boolean ->
-                        getFaceIconViewAsset(
-                            authState,
-                            isAuthenticating,
-                            isPendingConfirmation,
-                            showingError
-                        )
+                    shouldPulseAnimation.flatMapLatest { shouldPulseAnimation: Boolean ->
+                        if (shouldPulseAnimation) {
+                            val iconAsset =
+                                if (_lastPulseLightToDark.value) {
+                                    R.drawable.face_dialog_pulse_dark_to_light
+                                } else {
+                                    R.drawable.face_dialog_pulse_light_to_dark
+                                }
+                            _lastPulseLightToDark.value = !_lastPulseLightToDark.value
+                            flowOf(iconAsset)
+                        } else {
+                            combine(
+                                promptViewModel.isAuthenticated.distinctUntilChanged(),
+                                promptViewModel.isAuthenticating.distinctUntilChanged(),
+                                promptViewModel.isPendingConfirmation.distinctUntilChanged(),
+                                promptViewModel.showingError.distinctUntilChanged()
+                            ) {
+                                authState: PromptAuthState,
+                                isAuthenticating: Boolean,
+                                isPendingConfirmation: Boolean,
+                                showingError: Boolean ->
+                                getFaceIconViewAsset(
+                                    authState,
+                                    isAuthenticating,
+                                    isPendingConfirmation,
+                                    showingError
+                                )
+                            }
+                        }
                     }
                 AuthType.Coex ->
                     combine(
@@ -279,7 +327,8 @@
         } else if (authState.isAuthenticated) {
             R.drawable.face_dialog_dark_to_checkmark
         } else if (isAuthenticating) {
-            R.raw.face_dialog_authenticating
+            _lastPulseLightToDark.value = false
+            R.drawable.face_dialog_pulse_dark_to_light
         } else if (showingError) {
             R.drawable.face_dialog_dark_to_error
         } else if (_previousIconWasError.value) {
@@ -654,6 +703,16 @@
             }
         }
 
+    /** Whether the current BiometricPromptLayout.iconView asset animation should be repeated. */
+    val shouldRepeatAnimation: Flow<Boolean> =
+        activeAuthType.flatMapLatest { activeAuthType: AuthType ->
+            when (activeAuthType) {
+                AuthType.Fingerprint,
+                AuthType.Coex -> flowOf(false)
+                AuthType.Face -> promptViewModel.isAuthenticating.map { it }
+            }
+        }
+
     /** Called on configuration changes */
     fun onConfigurationChanged(newConfig: Configuration) {
         displayStateInteractor.onConfigurationChanged(newConfig)
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt b/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt
index 2b9fc73..7a9429e 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/dagger/ScreenBrightnessModule.kt
@@ -20,8 +20,15 @@
 import com.android.systemui.brightness.data.repository.BrightnessPolicyRepositoryImpl
 import com.android.systemui.brightness.data.repository.ScreenBrightnessDisplayManagerRepository
 import com.android.systemui.brightness.data.repository.ScreenBrightnessRepository
+import com.android.systemui.brightness.shared.model.BrightnessLog
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
 
 @Module
 interface ScreenBrightnessModule {
@@ -33,4 +40,20 @@
 
     @Binds
     fun bindPolicyRepository(impl: BrightnessPolicyRepositoryImpl): BrightnessPolicyRepository
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        @BrightnessLog
+        fun providesBrightnessTableLog(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("BrightnessTableLog", 50)
+        }
+
+        @Provides
+        @SysUISingleton
+        @BrightnessLog
+        fun providesBrightnessLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("BrightnessLog", 50)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/model/LinearBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/model/LinearBrightness.kt
deleted file mode 100644
index 608f301..0000000
--- a/packages/SystemUI/src/com/android/systemui/brightness/data/model/LinearBrightness.kt
+++ /dev/null
@@ -1,30 +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.systemui.brightness.data.model
-
-@JvmInline
-value class LinearBrightness(val floatValue: Float) {
-    fun clamp(min: LinearBrightness, max: LinearBrightness): LinearBrightness {
-        return if (floatValue < min.floatValue) {
-            min
-        } else if (floatValue > max.floatValue) {
-            max
-        } else {
-            this
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
index 9ed11d1..37d1887 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt
@@ -19,12 +19,18 @@
 import android.annotation.SuppressLint
 import android.hardware.display.BrightnessInfo
 import android.hardware.display.DisplayManager
-import com.android.systemui.brightness.data.model.LinearBrightness
+import com.android.systemui.brightness.shared.model.BrightnessLog
+import com.android.systemui.brightness.shared.model.LinearBrightness
+import com.android.systemui.brightness.shared.model.formatBrightness
+import com.android.systemui.brightness.shared.model.logDiffForTable
 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.DisplayId
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.table.TableLogBuffer
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
@@ -32,13 +38,13 @@
 import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -78,6 +84,8 @@
 constructor(
     @DisplayId private val displayId: Int,
     private val displayManager: DisplayManager,
+    @BrightnessLog private val logBuffer: LogBuffer,
+    @BrightnessLog private val tableBuffer: TableLogBuffer,
     @Application private val applicationScope: CoroutineScope,
     @Background private val backgroundContext: CoroutineContext,
 ) : ScreenBrightnessRepository {
@@ -100,6 +108,7 @@
                         displayManager.setBrightness(displayId, value)
                     }
                 }
+                logBrightnessChange(call is SetBrightnessMethod.Permanent, value)
             }
         }
     }
@@ -147,13 +156,15 @@
         brightnessInfo
             .filterNotNull()
             .map { LinearBrightness(it.brightnessMinimum) }
-            .shareIn(applicationScope, SharingStarted.WhileSubscribed())
+            .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MIN, null)
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f))
 
-    override val maxLinearBrightness =
+    override val maxLinearBrightness: SharedFlow<LinearBrightness> =
         brightnessInfo
             .filterNotNull()
             .map { LinearBrightness(it.brightnessMaximum) }
-            .shareIn(applicationScope, SharingStarted.WhileSubscribed())
+            .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_MAX, null)
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(1f))
 
     override suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness> {
         val brightnessInfo = brightnessInfo.value ?: brightnessInfoValue()
@@ -166,7 +177,8 @@
         brightnessInfo
             .filterNotNull()
             .map { LinearBrightness(it.brightness) }
-            .shareIn(applicationScope, SharingStarted.WhileSubscribed())
+            .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_BRIGHTNESS, null)
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f))
 
     override fun setTemporaryBrightness(value: LinearBrightness) {
         apiQueue.trySend(SetBrightnessMethod.Temporary(value))
@@ -183,4 +195,21 @@
         @JvmInline
         value class Permanent(override val value: LinearBrightness) : SetBrightnessMethod
     }
+
+    private fun logBrightnessChange(permanent: Boolean, value: Float) {
+        logBuffer.log(
+            LOG_BUFFER_BRIGHTNESS_CHANGE_TAG,
+            if (permanent) LogLevel.DEBUG else LogLevel.VERBOSE,
+            { str1 = value.formatBrightness() },
+            { "Change requested: $str1" }
+        )
+    }
+
+    private companion object {
+        const val TABLE_COLUMN_BRIGHTNESS = "brightness"
+        const val TABLE_COLUMN_MIN = "min"
+        const val TABLE_COLUMN_MAX = "max"
+        const val TABLE_PREFIX_LINEAR = "linear"
+        const val LOG_BUFFER_BRIGHTNESS_CHANGE_TAG = "BrightnessChange"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt
index 799a0a1..5647f521 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt
@@ -17,12 +17,20 @@
 package com.android.systemui.brightness.domain.interactor
 
 import com.android.settingslib.display.BrightnessUtils
-import com.android.systemui.brightness.data.model.LinearBrightness
 import com.android.systemui.brightness.data.repository.ScreenBrightnessRepository
-import com.android.systemui.brightness.shared.GammaBrightness
+import com.android.systemui.brightness.shared.model.BrightnessLog
+import com.android.systemui.brightness.shared.model.GammaBrightness
+import com.android.systemui.brightness.shared.model.LinearBrightness
+import com.android.systemui.brightness.shared.model.logDiffForTable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
 
 /**
  * Converts between [GammaBrightness] and [LinearBrightness].
@@ -34,6 +42,8 @@
 @Inject
 constructor(
     private val screenBrightnessRepository: ScreenBrightnessRepository,
+    @Application private val applicationScope: CoroutineScope,
+    @BrightnessLog private val tableBuffer: TableLogBuffer,
 ) {
     /** Maximum value in the Gamma space for brightness */
     val maxGammaBrightness = GammaBrightness(BrightnessUtils.GAMMA_SPACE_MAX)
@@ -45,15 +55,17 @@
      * Brightness in the Gamma space for the current display. It will always represent a value
      * between [minGammaBrightness] and [maxGammaBrightness]
      */
-    val gammaBrightness =
+    val gammaBrightness: Flow<GammaBrightness> =
         with(screenBrightnessRepository) {
             combine(
-                linearBrightness,
-                minLinearBrightness,
-                maxLinearBrightness,
-            ) { brightness, min, max ->
-                brightness.toGammaBrightness(min, max)
-            }
+                    linearBrightness,
+                    minLinearBrightness,
+                    maxLinearBrightness,
+                ) { brightness, min, max ->
+                    brightness.toGammaBrightness(min, max)
+                }
+                .logDiffForTable(tableBuffer, TABLE_PREFIX_GAMMA, TABLE_COLUMN_BRIGHTNESS, null)
+                .stateIn(applicationScope, SharingStarted.WhileSubscribed(), GammaBrightness(0))
         }
 
     /** Sets the brightness temporarily, while the user is changing it. */
@@ -91,4 +103,9 @@
             BrightnessUtils.convertLinearToGammaFloat(floatValue, min.floatValue, max.floatValue)
         )
     }
+
+    private companion object {
+        const val TABLE_COLUMN_BRIGHTNESS = "brightness"
+        const val TABLE_PREFIX_GAMMA = "gamma"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/GammaBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/GammaBrightness.kt
deleted file mode 100644
index e20d003..0000000
--- a/packages/SystemUI/src/com/android/systemui/brightness/shared/GammaBrightness.kt
+++ /dev/null
@@ -1,29 +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.systemui.brightness.shared
-
-import androidx.annotation.IntRange
-import com.android.settingslib.display.BrightnessUtils
-
-@JvmInline
-value class GammaBrightness(
-    @IntRange(
-        from = BrightnessUtils.GAMMA_SPACE_MIN.toLong(),
-        to = BrightnessUtils.GAMMA_SPACE_MAX.toLong()
-    )
-    val value: Int
-)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/BrightnessLog.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
copy to packages/SystemUI/src/com/android/systemui/brightness/shared/model/BrightnessLog.kt
index 0854e93..b514fef 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/BrightnessLog.kt
@@ -13,14 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.dreams.homecontrols
 
-import android.app.Activity
-import android.service.dreams.DreamService
-import javax.inject.Inject
+package com.android.systemui.brightness.shared.model
 
-class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
-    override fun getActivity(dreamService: DreamService): Activity {
-        return dreamService.activity
-    }
-}
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class BrightnessLog()
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt
new file mode 100644
index 0000000..7eba626
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/GammaBrightness.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.brightness.shared.model
+
+import androidx.annotation.IntRange
+import com.android.settingslib.display.BrightnessUtils
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.kotlin.pairwiseBy
+import kotlinx.coroutines.flow.Flow
+
+@JvmInline
+value class GammaBrightness(
+    @IntRange(
+        from = BrightnessUtils.GAMMA_SPACE_MIN.toLong(),
+        to = BrightnessUtils.GAMMA_SPACE_MAX.toLong()
+    )
+    val value: Int
+)
+
+internal fun Flow<GammaBrightness>.logDiffForTable(
+    tableLogBuffer: TableLogBuffer,
+    columnPrefix: String,
+    columnName: String,
+    initialValue: GammaBrightness?,
+): Flow<GammaBrightness> {
+    val initialValueFun = {
+        tableLogBuffer.logChange(columnPrefix, columnName, initialValue?.value, isInitial = true)
+        initialValue
+    }
+    return this.pairwiseBy(initialValueFun) { prevVal: GammaBrightness?, newVal: GammaBrightness ->
+        if (prevVal != newVal) {
+            tableLogBuffer.logChange(columnPrefix, columnName, newVal.value)
+        }
+        newVal
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.kt b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.kt
new file mode 100644
index 0000000..1c886e6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/brightness/shared/model/LinearBrightness.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.brightness.shared.model
+
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.kotlin.pairwiseBy
+import kotlinx.coroutines.flow.Flow
+
+@JvmInline
+value class LinearBrightness(val floatValue: Float) {
+    fun clamp(min: LinearBrightness, max: LinearBrightness): LinearBrightness {
+        return if (floatValue < min.floatValue) {
+            min
+        } else if (floatValue > max.floatValue) {
+            max
+        } else {
+            this
+        }
+    }
+
+    val loggableString: String
+        get() = floatValue.formatBrightness()
+}
+
+fun Float.formatBrightness(): String {
+    return "%.3f".format(this)
+}
+
+internal fun Flow<LinearBrightness>.logDiffForTable(
+    tableLogBuffer: TableLogBuffer,
+    columnPrefix: String,
+    columnName: String,
+    initialValue: LinearBrightness?,
+): Flow<LinearBrightness> {
+    val initialValueFun = {
+        tableLogBuffer.logChange(
+            columnPrefix,
+            columnName,
+            initialValue?.loggableString,
+            isInitial = true
+        )
+        initialValue
+    }
+    return this.pairwiseBy(initialValueFun) { prevVal: LinearBrightness?, newVal: LinearBrightness
+        ->
+        if (prevVal != newVal) {
+            tableLogBuffer.logChange(columnPrefix, columnName, newVal.loggableString)
+        }
+        newVal
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
index a51d8ff..f991d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
@@ -33,14 +33,13 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.PlatformSlider
-import com.android.systemui.brightness.shared.GammaBrightness
+import com.android.systemui.brightness.shared.model.GammaBrightness
 import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
 import com.android.systemui.brightness.ui.viewmodel.Drag
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.utils.PolicyRestriction
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
 @Composable
@@ -107,8 +106,8 @@
     viewModel: BrightnessSliderViewModel,
     modifier: Modifier = Modifier,
 ) {
-    val gamma: Int by
-        viewModel.currentBrightness.map { it.value }.collectAsStateWithLifecycle(initialValue = 0)
+    val state by viewModel.currentBrightness.collectAsStateWithLifecycle()
+    val gamma = state.value
     val coroutineScope = rememberCoroutineScope()
     val restriction by
         viewModel.policyRestriction.collectAsStateWithLifecycle(
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
index f0988ba..16a1dcc 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
@@ -18,14 +18,18 @@
 
 import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor
 import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInteractor
-import com.android.systemui.brightness.shared.GammaBrightness
+import com.android.systemui.brightness.shared.model.GammaBrightness
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
 import com.android.systemui.utils.PolicyRestriction
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
 
 @SysUISingleton
 class BrightnessSliderViewModel
@@ -33,8 +37,14 @@
 constructor(
     private val screenBrightnessInteractor: ScreenBrightnessInteractor,
     private val brightnessPolicyEnforcementInteractor: BrightnessPolicyEnforcementInteractor,
+    @Application private val applicationScope: CoroutineScope,
 ) {
-    val currentBrightness = screenBrightnessInteractor.gammaBrightness
+    val currentBrightness =
+        screenBrightnessInteractor.gammaBrightness.stateIn(
+            applicationScope,
+            SharingStarted.WhileSubscribed(),
+            GammaBrightness(0)
+        )
 
     val maxBrightness = screenBrightnessInteractor.maxGammaBrightness
     val minBrightness = screenBrightnessInteractor.minGammaBrightness
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index 153b7aa..8993a3b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint
 import android.app.DreamManager
 import com.android.systemui.CoreStartable
+import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
 import com.android.systemui.Flags.communalHub
 import com.android.systemui.Flags.restartDreamOnUnocclude
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -30,11 +31,11 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample
 import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import javax.inject.Inject
 
 /**
  * A [CoreStartable] responsible for automatically starting the dream when the communal hub is
@@ -78,6 +79,7 @@
                 if (
                     finishedState == KeyguardState.GLANCEABLE_HUB &&
                         !dreaming &&
+                        !glanceableHubAllowKeyguardWhenDreaming() &&
                         dreamManager.canStartDreaming(isAwake)
                 ) {
                     dreamManager.startDream()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 3d9e861..8cd5603 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -91,6 +91,12 @@
     /** A list of all the communal content to be displayed in the communal hub. */
     abstract val communalContent: Flow<List<CommunalContentModel>>
 
+    /**
+     * Whether to freeze the emission of the communalContent flow to prevent recomposition. Defaults
+     * to false, indicating that the flow will emit new update.
+     */
+    open val isCommunalContentFlowFrozen: Flow<Boolean> = flowOf(false)
+
     /** Whether in edit mode for the communal hub. */
     open val isEditMode = false
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 7f3a2dc..ce69ee8 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -38,7 +38,9 @@
 import com.android.systemui.media.dagger.MediaModule
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
 import javax.inject.Inject
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineScope
@@ -76,8 +78,11 @@
 
     private val logger = Logger(logBuffer, "CommunalViewModel")
 
+    /** Communal content saved from the previous emission when the flow is active (not "frozen"). */
+    private var frozenCommunalContent: List<CommunalContentModel>? = null
+
     @OptIn(ExperimentalCoroutinesApi::class)
-    override val communalContent: Flow<List<CommunalContentModel>> =
+    private val latestCommunalContent: Flow<List<CommunalContentModel>> =
         tutorialInteractor.isTutorialAvailable
             .flatMapLatest { isTutorialMode ->
                 if (isTutorialMode) {
@@ -93,9 +98,40 @@
                 }
             }
             .onEach { models ->
+                frozenCommunalContent = models
                 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
             }
 
+    /**
+     * Freeze the content flow, when an activity is about to show, like starting a timer via voice:
+     * 1) in handheld mode, use the keyguard occluded state;
+     * 2) in dreaming mode, where keyguard is already occluded by dream, use the dream wakeup
+     *    signal. Since in this case the shell transition info does not include
+     *    KEYGUARD_VISIBILITY_TRANSIT_FLAGS, KeyguardTransitionHandler will not run the
+     *    occludeAnimation on KeyguardViewMediator.
+     */
+    override val isCommunalContentFlowFrozen: Flow<Boolean> =
+        allOf(
+                keyguardTransitionInteractor.isFinishedInState(KeyguardState.GLANCEABLE_HUB),
+                keyguardInteractor.isKeyguardOccluded,
+                not(keyguardInteractor.isAbleToDream)
+            )
+            .distinctUntilChanged()
+            .onEach { logger.d("isCommunalContentFlowFrozen: $it") }
+
+    override val communalContent: Flow<List<CommunalContentModel>> =
+        isCommunalContentFlowFrozen
+            .flatMapLatestConflated { isFrozen ->
+                if (isFrozen) {
+                    flowOf(frozenCommunalContent ?: emptyList())
+                } else {
+                    latestCommunalContent
+                }
+            }
+            .onEach { models ->
+                logger.d({ "CommunalContent: $str1" }) { str1 = models.joinToString { it.key } }
+            }
+
     override val isEmptyState: Flow<Boolean> =
         communalInteractor.widgetContent
             .map { it.isEmpty() }
diff --git a/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java b/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
index 3cf22b1..a679bfb 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/OpenHubComplication.java
@@ -67,7 +67,7 @@
     @Override
     public int getRequiredTypeAvailability() {
         // TODO(b/339667383): create a new complication type if we decide to productionize this
-        return COMPLICATION_TYPE_HOME_CONTROLS;
+        return COMPLICATION_TYPE_NONE;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt b/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt
index 4dbb32d..1bbdfcd 100644
--- a/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt
+++ b/packages/SystemUI/src/com/android/systemui/dock/DockManagerExtensions.kt
@@ -19,16 +19,18 @@
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 
 /**
- * Retrieves whether or not the device is docked according to DockManager. Emits a starting value
- *  of isDocked.
+ * Retrieves whether or not the device is docked according to DockManager. Emits a starting value of
+ * isDocked.
  */
 fun DockManager.retrieveIsDocked(): Flow<Boolean> =
     ConflatedCallbackFlow.conflatedCallbackFlow {
-        val callback = DockManager.DockEventListener { trySend(isDocked) }
-        addListener(callback)
-        trySend(isDocked)
+            val callback = DockManager.DockEventListener { trySend(isDocked) }
+            addListener(callback)
+            trySend(isDocked)
 
-        awaitClose { removeListener(callback) }
-    }
\ No newline at end of file
+            awaitClose { removeListener(callback) }
+        }
+        .distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 96e708f..c6c57479 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -18,6 +18,7 @@
 
 import static android.service.dreams.Flags.dreamWakeRedirect;
 
+import static com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming;
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
 import static com.android.systemui.dreams.dagger.DreamModule.DREAM_TOUCH_INSET_MANAGER;
 import static com.android.systemui.dreams.dagger.DreamModule.HOME_CONTROL_PANEL_DREAM_COMPONENT;
@@ -395,7 +396,7 @@
             return;
         }
 
-        redirectWake(mCommunalAvailable);
+        redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index b0d134f..f6ac7a5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -33,8 +33,8 @@
 import com.android.systemui.dreams.DreamOverlayService;
 import com.android.systemui.dreams.SystemDialogsCloser;
 import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
-import com.android.systemui.dreams.homecontrols.DreamActivityProvider;
-import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl;
+import com.android.systemui.dreams.homecontrols.DreamServiceDelegate;
+import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl;
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.pipeline.shared.TileSpec;
@@ -202,8 +202,8 @@
     }
 
 
-    /** Provides activity for dream service */
+    /** Provides delegate to allow for testing of dream service */
     @Binds
-    DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl);
+    DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl);
 
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
deleted file mode 100644
index b35b7f5..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt
+++ /dev/null
@@ -1,27 +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.systemui.dreams.homecontrols
-
-import android.app.Activity
-import android.service.dreams.DreamService
-
-fun interface DreamActivityProvider {
-    /**
-     * Provides abstraction for getting the activity associated with a dream service, so that the
-     * activity can be mocked in tests.
-     */
-    fun getActivity(dreamService: DreamService): Activity?
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt
new file mode 100644
index 0000000..2cfb02e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.dreams.homecontrols
+
+import android.app.Activity
+import android.service.dreams.DreamService
+
+/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */
+interface DreamServiceDelegate {
+    /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */
+    fun getActivity(dreamService: DreamService): Activity?
+
+    /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */
+    fun wakeUp(dreamService: DreamService)
+
+    /** Wrapper for [DreamService.finish] which can be mocked in tests. */
+    fun finish(dreamService: DreamService)
+
+    /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */
+    fun redirectWake(dreamService: DreamService): Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt
similarity index 69%
rename from packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
rename to packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt
index 0854e93..7dc5434 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt
@@ -19,8 +19,20 @@
 import android.service.dreams.DreamService
 import javax.inject.Inject
 
-class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
+class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate {
     override fun getActivity(dreamService: DreamService): Activity {
         return dreamService.activity
     }
+
+    override fun finish(dreamService: DreamService) {
+        dreamService.finish()
+    }
+
+    override fun wakeUp(dreamService: DreamService) {
+        dreamService.wakeUp()
+    }
+
+    override fun redirectWake(dreamService: DreamService): Boolean {
+        return dreamService.redirectWake
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
index 76187c6..77c54ec 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.util.wakelock.WakeLock
 import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT
 import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -46,7 +47,7 @@
     private val taskFragmentFactory: TaskFragmentComponent.Factory,
     private val homeControlsComponentInteractor: HomeControlsComponentInteractor,
     private val wakeLockBuilder: WakeLock.Builder,
-    private val dreamActivityProvider: DreamActivityProvider,
+    private val dreamServiceDelegate: DreamServiceDelegate,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @DreamLog logBuffer: LogBuffer
 ) : DreamService() {
@@ -65,7 +66,7 @@
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
-        val activity = dreamActivityProvider.getActivity(this)
+        val activity = dreamServiceDelegate.getActivity(this)
         if (activity == null) {
             finish()
             return
@@ -79,9 +80,9 @@
             taskFragmentFactory
                 .create(
                     activity = activity,
-                    onCreateCallback = this::onTaskFragmentCreated,
+                    onCreateCallback = { launchActivity() },
                     onInfoChangedCallback = this::onTaskFragmentInfoChanged,
-                    hide = { endDream() }
+                    hide = { endDream(false) }
                 )
                 .apply { createTaskFragment() }
 
@@ -91,16 +92,24 @@
     private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) {
         if (taskFragmentInfo.isEmpty) {
             logger.d("Finishing dream due to TaskFragment being empty")
-            endDream()
+            endDream(true)
         }
     }
 
-    private fun endDream() {
+    private fun endDream(handleRedirect: Boolean) {
         homeControlsComponentInteractor.onDreamEndUnexpectedly()
-        finish()
+        if (handleRedirect && dreamServiceDelegate.redirectWake(this)) {
+            dreamServiceDelegate.wakeUp(this)
+            serviceScope.launch {
+                delay(ACTIVITY_RESTART_DELAY)
+                launchActivity()
+            }
+        } else {
+            dreamServiceDelegate.finish(this)
+        }
     }
 
-    private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) {
+    private fun launchActivity() {
         val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value
         val componentName = homeControlsComponentInteractor.panelComponent.value
         logger.d("Starting embedding $componentName")
@@ -134,6 +143,14 @@
          * complete.
          */
         val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds
+
+        /**
+         * Defines the delay after wakeup where we should attempt to restart the embedded activity.
+         * When a wakeup is redirected, the dream service may keep running. In this case, we should
+         * restart the activity if it finished. This delays ensures the activity is only restarted
+         * after the wakeup transition has played.
+         */
+        val ACTIVITY_RESTART_DELAY = 334.milliseconds
         const val TAG = "HomeControlsDreamService"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
index c5b3c53..4b07f78 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.dreams.ui.viewmodel
 
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
@@ -60,7 +61,7 @@
         val showGlanceableHub =
             communalInteractor.isCommunalEnabled.value &&
                 !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId)
-        if (showGlanceableHub) {
+        if (showGlanceableHub && !glanceableHubAllowKeyguardWhenDreaming()) {
             communalInteractor.changeScene(CommunalScenes.Communal)
         } else {
             toLockscreenTransitionViewModel.startTransition()
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index c084340..3d3584e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -60,14 +60,6 @@
             "notification_drag_to_contents"
         )
 
-    /**
-     * This flag controls whether we register a listener for StatsD notification memory reports.
-     * For statsd to actually call the listener however, a server-side toggle needs to be
-     * enabled as well.
-     */
-    val NOTIFICATION_MEMORY_LOGGING_ENABLED =
-            releasedFlag("notification_memory_logging_enabled")
-
     // TODO(b/280783617): Tracking Bug
     @Keep
     @JvmField
@@ -386,9 +378,6 @@
     val WARN_ON_BLOCKING_BINDER_TRANSACTIONS =
         unreleasedFlag("warn_on_blocking_binder_transactions")
 
-    // TODO:(b/283203305): Tracking bug
-    @JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag("trim_font_caches_on_unlock")
-
     // TODO(b/298380520): Tracking Bug
     @JvmField
     val USER_TRACKER_BACKGROUND_CALLBACKS = unreleasedFlag("user_tracker_background_callbacks")
@@ -459,14 +448,6 @@
     @JvmField
     val ENABLE_CLOCK_KEYGUARD_PRESENTATION = releasedFlag("enable_clock_keyguard_presentation")
 
-    /** Enable the Compose implementation of the PeopleSpaceActivity. */
-    @JvmField
-    val COMPOSE_PEOPLE_SPACE = releasedFlag("compose_people_space")
-
-    /** Enable the Compose implementation of the Quick Settings footer actions. */
-    @JvmField
-    val COMPOSE_QS_FOOTER_ACTIONS = releasedFlag("compose_qs_footer_actions")
-
     /** Enable the share wifi button in Quick Settings internet dialog. */
     @JvmField
     val SHARE_WIFI_QS_BUTTON = releasedFlag("share_wifi_qs_button")
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
index ea8d7d7..30b9583 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt
@@ -19,7 +19,9 @@
 import android.os.VibrationEffect
 import android.view.View
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.animation.Expandable
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.statusbar.VibratorHelper
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -51,6 +53,10 @@
     var state = State.IDLE
         private set
 
+    /** The QSTile and Expandable used to perform a long-click and click actions */
+    var qsTile: QSTile? = null
+    var expandable: Expandable? = null
+
     /** Flow for view control and action */
     private val _postedActionType = MutableStateFlow<ActionType?>(null)
     val actionType: Flow<ActionType?> =
@@ -105,6 +111,7 @@
         when (state) {
             State.IDLE -> {
                 setState(State.TIMEOUT_WAIT)
+                _postedActionType.value = ActionType.WAIT_TAP_TIMEOUT
             }
             State.RUNNING_BACKWARDS -> _postedActionType.value = ActionType.CANCEL_ANIMATOR
             else -> {}
@@ -112,16 +119,9 @@
     }
 
     fun handleActionUp() {
-        when (state) {
-            State.TIMEOUT_WAIT -> {
-                _postedActionType.value = ActionType.CLICK
-                setState(State.IDLE)
-            }
-            State.RUNNING_FORWARD -> {
-                _postedActionType.value = ActionType.REVERSE_ANIMATOR
-                setState(State.RUNNING_BACKWARDS)
-            }
-            else -> {}
+        if (state == State.RUNNING_FORWARD) {
+            _postedActionType.value = ActionType.REVERSE_ANIMATOR
+            setState(State.RUNNING_BACKWARDS)
         }
     }
 
@@ -129,6 +129,7 @@
         when (state) {
             State.TIMEOUT_WAIT -> {
                 setState(State.IDLE)
+                clearActionType()
             }
             State.RUNNING_FORWARD -> {
                 _postedActionType.value = ActionType.REVERSE_ANIMATOR
@@ -145,18 +146,23 @@
 
     /** This function is called both when an animator completes or gets cancelled */
     fun handleAnimationComplete() {
-        if (state == State.RUNNING_FORWARD) {
-            vibrate(snapEffect)
-            _postedActionType.value = ActionType.LONG_PRESS
-        }
-        if (state != State.TIMEOUT_WAIT) {
-            // This will happen if the animator did not finish by being cancelled
-            setState(State.IDLE)
+        when (state) {
+            State.RUNNING_FORWARD -> {
+                setState(State.IDLE)
+                vibrate(snapEffect)
+                _postedActionType.value = ActionType.LONG_PRESS
+            }
+            State.RUNNING_BACKWARDS -> {
+                setState(State.IDLE)
+                clearActionType()
+            }
+            else -> {}
         }
     }
 
     fun handleAnimationCancel() {
         setState(State.TIMEOUT_WAIT)
+        _postedActionType.value = ActionType.WAIT_TAP_TIMEOUT
     }
 
     fun handleTimeoutComplete() {
@@ -190,9 +196,22 @@
                 effectDuration
             )
         setState(State.IDLE)
+        clearActionType()
         return true
     }
 
+    fun onTileClick(): Boolean {
+        if (state == State.TIMEOUT_WAIT) {
+            setState(State.IDLE)
+            clearActionType()
+            qsTile?.let {
+                it.click(expandable)
+                return true
+            }
+        }
+        return false
+    }
+
     enum class State {
         IDLE, /* The effect is idle waiting for touch input */
         TIMEOUT_WAIT, /* The effect is waiting for a [PRESSED_TIMEOUT] period */
@@ -202,7 +221,7 @@
 
     /* A type of action to perform on the view depending on the effect's state and logic */
     enum class ActionType {
-        CLICK,
+        WAIT_TAP_TIMEOUT,
         LONG_PRESS,
         RESET_AND_LONG_PRESS,
         START_ANIMATOR,
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
index 4875f48..92a55ef 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt
@@ -17,8 +17,6 @@
 package com.android.systemui.haptics.qs
 
 import android.animation.ValueAnimator
-import android.annotation.SuppressLint
-import android.view.MotionEvent
 import android.view.ViewConfiguration
 import android.view.animation.AccelerateDecelerateInterpolator
 import androidx.core.animation.doOnCancel
@@ -30,6 +28,7 @@
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.qs.tileimpl.QSTileViewImpl
 import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.filterNotNull
 
 object QSLongPressEffectViewBinder {
@@ -41,9 +40,6 @@
     ): DisposableHandle? {
         if (qsLongPressEffect == null) return null
 
-        // Set the touch listener as the long-press effect
-        setTouchListener(tile, qsLongPressEffect)
-
         return tile.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 // Action to perform
@@ -52,18 +48,18 @@
 
                     qsLongPressEffect.actionType.filterNotNull().collect { action ->
                         when (action) {
-                            QSLongPressEffect.ActionType.CLICK -> {
-                                tile.performClick()
-                                qsLongPressEffect.clearActionType()
+                            QSLongPressEffect.ActionType.WAIT_TAP_TIMEOUT -> {
+                                delay(ViewConfiguration.getTapTimeout().toLong())
+                                qsLongPressEffect.handleTimeoutComplete()
                             }
                             QSLongPressEffect.ActionType.LONG_PRESS -> {
                                 tile.prepareForLaunch()
-                                tile.performLongClick()
+                                qsLongPressEffect.qsTile?.longClick(qsLongPressEffect.expandable)
                                 qsLongPressEffect.clearActionType()
                             }
                             QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> {
                                 tile.resetLongPressEffectProperties()
-                                tile.performLongClick()
+                                qsLongPressEffect.qsTile?.longClick(qsLongPressEffect.expandable)
                                 qsLongPressEffect.clearActionType()
                             }
                             QSLongPressEffect.ActionType.START_ANIMATOR -> {
@@ -106,22 +102,4 @@
             }
         }
     }
-
-    @SuppressLint("ClickableViewAccessibility")
-    private fun setTouchListener(tile: QSTileViewImpl, longPressEffect: QSLongPressEffect?) {
-        tile.setOnTouchListener { _, event ->
-            when (event.actionMasked) {
-                MotionEvent.ACTION_DOWN -> {
-                    tile.postDelayed(
-                        { longPressEffect?.handleTimeoutComplete() },
-                        ViewConfiguration.getTapTimeout().toLong(),
-                    )
-                    longPressEffect?.handleActionDown()
-                }
-                MotionEvent.ACTION_UP -> longPressEffect?.handleActionUp()
-                MotionEvent.ACTION_CANCEL -> longPressEffect?.handleActionCancel()
-            }
-            true
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
new file mode 100644
index 0000000..52ccc21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -0,0 +1,413 @@
+/*
+ * 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.keyboard.shortcut.ui.composable
+
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+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
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.OpenInNew
+import androidx.compose.material.icons.filled.Accessibility
+import androidx.compose.material.icons.filled.Apps
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.Keyboard
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Tv
+import androidx.compose.material.icons.filled.VerticalSplit
+import androidx.compose.material3.CenterAlignedTopAppBar
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationDrawerItemColors
+import androidx.compose.material3.NavigationDrawerItemDefaults
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
+import com.android.systemui.res.R
+
+@Composable
+fun ShortcutHelper(modifier: Modifier = Modifier, onKeyboardSettingsClicked: () -> Unit) {
+    if (shouldUseSinglePane()) {
+        ShortcutHelperSinglePane(modifier, categories, onKeyboardSettingsClicked)
+    } else {
+        ShortcutHelperTwoPane(modifier, categories, onKeyboardSettingsClicked)
+    }
+}
+
+@Composable
+private fun shouldUseSinglePane() =
+    LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact
+
+@Composable
+private fun ShortcutHelperSinglePane(
+    modifier: Modifier = Modifier,
+    categories: List<ShortcutHelperCategory>,
+    onKeyboardSettingsClicked: () -> Unit,
+) {
+    Column(
+        modifier =
+            modifier
+                .fillMaxSize()
+                .verticalScroll(rememberScrollState())
+                .padding(start = 16.dp, end = 16.dp, top = 26.dp)
+    ) {
+        TitleBar()
+        Spacer(modifier = Modifier.height(6.dp))
+        ShortcutsSearchBar()
+        Spacer(modifier = Modifier.height(16.dp))
+        CategoriesPanelSinglePane(categories)
+        Spacer(modifier = Modifier.weight(1f))
+        KeyboardSettings(onClick = onKeyboardSettingsClicked)
+    }
+}
+
+@Composable
+private fun CategoriesPanelSinglePane(
+    categories: List<ShortcutHelperCategory>,
+) {
+    var expandedCategory by remember { mutableStateOf<ShortcutHelperCategory?>(null) }
+    Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
+        categories.fastForEachIndexed { index, category ->
+            val isExpanded = expandedCategory == category
+            val itemShape =
+                if (index == 0) {
+                    ShortcutHelper.Shapes.singlePaneFirstCategory
+                } else if (index == categories.lastIndex) {
+                    ShortcutHelper.Shapes.singlePaneLastCategory
+                } else {
+                    ShortcutHelper.Shapes.singlePaneCategory
+                }
+            CategoryItemSinglePane(
+                category = category,
+                isExpanded = isExpanded,
+                onClick = {
+                    expandedCategory =
+                        if (isExpanded) {
+                            null
+                        } else {
+                            category
+                        }
+                },
+                shape = itemShape,
+            )
+        }
+    }
+}
+
+@Composable
+private fun CategoryItemSinglePane(
+    category: ShortcutHelperCategory,
+    isExpanded: Boolean,
+    onClick: () -> Unit,
+    shape: Shape,
+) {
+    Surface(
+        color = MaterialTheme.colorScheme.surfaceBright,
+        shape = shape,
+        onClick = onClick,
+    ) {
+        Column {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp).padding(horizontal = 16.dp)
+            ) {
+                Icon(category.icon, contentDescription = null)
+                Spacer(modifier = Modifier.width(16.dp))
+                Text(stringResource(category.labelResId))
+                Spacer(modifier = Modifier.weight(1f))
+                RotatingExpandCollapseIcon(isExpanded)
+            }
+            AnimatedVisibility(visible = isExpanded) { ShortcutCategoryDetailsSinglePane(category) }
+        }
+    }
+}
+
+@Composable
+private fun RotatingExpandCollapseIcon(isExpanded: Boolean) {
+    val expandIconRotationDegrees by
+        animateFloatAsState(
+            targetValue =
+                if (isExpanded) {
+                    180f
+                } else {
+                    0f
+                },
+            label = "Expand icon rotation animation"
+        )
+    Icon(
+        modifier =
+            Modifier.background(
+                    color = MaterialTheme.colorScheme.surfaceContainerHigh,
+                    shape = CircleShape
+                )
+                .graphicsLayer { rotationZ = expandIconRotationDegrees },
+        imageVector = Icons.Default.ExpandMore,
+        contentDescription =
+            if (isExpanded) {
+                stringResource(R.string.shortcut_helper_content_description_collapse_icon)
+            } else {
+                stringResource(R.string.shortcut_helper_content_description_expand_icon)
+            },
+        tint = MaterialTheme.colorScheme.onSurface
+    )
+}
+
+@Composable
+private fun ShortcutCategoryDetailsSinglePane(category: ShortcutHelperCategory) {
+    Box(modifier = Modifier.fillMaxWidth().heightIn(min = 300.dp)) {
+        Text(
+            modifier = Modifier.align(Alignment.Center),
+            text = stringResource(category.labelResId),
+        )
+    }
+}
+
+@Composable
+private fun ShortcutHelperTwoPane(
+    modifier: Modifier = Modifier,
+    categories: List<ShortcutHelperCategory>,
+    onKeyboardSettingsClicked: () -> Unit,
+) {
+    Column(modifier = modifier.fillMaxSize().padding(start = 24.dp, end = 24.dp, top = 26.dp)) {
+        TitleBar()
+        Spacer(modifier = Modifier.height(12.dp))
+        Row(Modifier.fillMaxWidth()) {
+            StartSidePanel(
+                modifier = Modifier.fillMaxWidth(fraction = 0.32f),
+                categories = categories,
+                onKeyboardSettingsClicked = onKeyboardSettingsClicked,
+            )
+            Spacer(modifier = Modifier.width(24.dp))
+            EndSidePanel(Modifier.fillMaxSize())
+        }
+    }
+}
+
+@Composable
+private fun StartSidePanel(
+    modifier: Modifier,
+    categories: List<ShortcutHelperCategory>,
+    onKeyboardSettingsClicked: () -> Unit,
+) {
+    Column(modifier) {
+        ShortcutsSearchBar()
+        Spacer(modifier = Modifier.heightIn(16.dp))
+        CategoriesPanelTwoPane(categories)
+        Spacer(modifier = Modifier.weight(1f))
+        KeyboardSettings(onKeyboardSettingsClicked)
+    }
+}
+
+@Composable
+private fun CategoriesPanelTwoPane(categories: List<ShortcutHelperCategory>) {
+    var selected by remember { mutableStateOf(categories.first()) }
+    Column {
+        categories.fastForEach {
+            CategoryItemTwoPane(
+                label = stringResource(it.labelResId),
+                icon = it.icon,
+                selected = selected == it,
+                onClick = { selected = it }
+            )
+        }
+    }
+}
+
+@Composable
+private fun CategoryItemTwoPane(
+    label: String,
+    icon: ImageVector,
+    selected: Boolean,
+    onClick: () -> Unit,
+    colors: NavigationDrawerItemColors =
+        NavigationDrawerItemDefaults.colors(unselectedContainerColor = Color.Transparent),
+) {
+    Surface(
+        selected = selected,
+        onClick = onClick,
+        modifier = Modifier.semantics { role = Role.Tab }.heightIn(min = 72.dp).fillMaxWidth(),
+        shape = RoundedCornerShape(28.dp),
+        color = colors.containerColor(selected).value,
+    ) {
+        Row(Modifier.padding(horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically) {
+            Icon(
+                modifier = Modifier.size(24.dp),
+                imageVector = icon,
+                contentDescription = null,
+                tint = colors.iconColor(selected).value
+            )
+            Spacer(Modifier.width(12.dp))
+            Box(Modifier.weight(1f)) {
+                Text(
+                    fontSize = 18.sp,
+                    color = colors.textColor(selected).value,
+                    style = MaterialTheme.typography.headlineSmall,
+                    text = label
+                )
+            }
+        }
+    }
+}
+
+@Composable
+fun EndSidePanel(modifier: Modifier) {
+    Surface(
+        modifier = modifier,
+        shape = RoundedCornerShape(28.dp),
+        color = MaterialTheme.colorScheme.surfaceBright
+    ) {}
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun TitleBar() {
+    CenterAlignedTopAppBar(
+        colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = Color.Transparent),
+        title = {
+            Text(
+                text = stringResource(R.string.shortcut_helper_title),
+                color = MaterialTheme.colorScheme.onSurface,
+                style = MaterialTheme.typography.headlineSmall
+            )
+        }
+    )
+}
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun ShortcutsSearchBar() {
+    var query by remember { mutableStateOf("") }
+    SearchBar(
+        colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
+        query = query,
+        active = false,
+        onActiveChange = {},
+        onQueryChange = { query = it },
+        onSearch = {},
+        leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
+        placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
+        content = {}
+    )
+}
+
+@Composable
+private fun KeyboardSettings(onClick: () -> Unit) {
+    Surface(
+        onClick = onClick,
+        shape = RoundedCornerShape(24.dp),
+        color = Color.Transparent,
+        modifier = Modifier.semantics { role = Role.Button }.fillMaxWidth()
+    ) {
+        Row(
+            modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(
+                "Keyboard Settings",
+                color = MaterialTheme.colorScheme.onSurfaceVariant,
+                fontSize = 16.sp
+            )
+            Spacer(modifier = Modifier.width(8.dp))
+            Icon(
+                imageVector = Icons.AutoMirrored.Default.OpenInNew,
+                contentDescription = null,
+                tint = MaterialTheme.colorScheme.onSurfaceVariant
+            )
+        }
+    }
+}
+
+/** Temporary data class just to populate the UI. */
+private data class ShortcutHelperCategory(
+    @StringRes val labelResId: Int,
+    val icon: ImageVector,
+)
+
+// Temporarily populating the categories directly in the UI.
+private val categories =
+    listOf(
+        ShortcutHelperCategory(R.string.shortcut_helper_category_system, Icons.Default.Tv),
+        ShortcutHelperCategory(
+            R.string.shortcut_helper_category_multitasking,
+            Icons.Default.VerticalSplit
+        ),
+        ShortcutHelperCategory(R.string.shortcut_helper_category_input, Icons.Default.Keyboard),
+        ShortcutHelperCategory(R.string.shortcut_helper_category_app_shortcuts, Icons.Default.Apps),
+        ShortcutHelperCategory(R.string.shortcut_helper_category_a11y, Icons.Default.Accessibility),
+    )
+
+object ShortcutHelper {
+
+    object Shapes {
+        val singlePaneFirstCategory =
+            RoundedCornerShape(
+                topStart = Dimensions.SinglePaneCategoryCornerRadius,
+                topEnd = Dimensions.SinglePaneCategoryCornerRadius
+            )
+        val singlePaneLastCategory =
+            RoundedCornerShape(
+                bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
+                bottomEnd = Dimensions.SinglePaneCategoryCornerRadius
+            )
+        val singlePaneCategory = RectangleShape
+    }
+
+    object Dimensions {
+        val SinglePaneCategoryCornerRadius = 28.dp
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index ef4156d..1e8d239 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -23,9 +23,12 @@
 import androidx.activity.BackEventCompat
 import androidx.activity.ComponentActivity
 import androidx.activity.OnBackPressedCallback
+import androidx.compose.ui.platform.ComposeView
 import androidx.core.view.updatePadding
 import androidx.lifecycle.flowWithLifecycle
 import androidx.lifecycle.lifecycleScope
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.keyboard.shortcut.ui.composable.ShortcutHelper
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.res.R
 import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -58,14 +61,30 @@
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_keyboard_shortcut_helper)
         setUpBottomSheetWidth()
+        expandBottomSheet()
         setUpInsets()
         setUpPredictiveBack()
         setUpSheetDismissListener()
         setUpDismissOnTouchOutside()
+        setUpComposeView()
         observeFinishRequired()
         viewModel.onViewOpened()
     }
 
+    private fun setUpComposeView() {
+        requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
+            setContent {
+                PlatformTheme {
+                    ShortcutHelper(
+                        onKeyboardSettingsClicked = ::onKeyboardSettingsClicked,
+                    )
+                }
+            }
+        }
+    }
+
+    private fun onKeyboardSettingsClicked() {}
+
     override fun onDestroy() {
         super.onDestroy()
         if (isFinishing) {
@@ -101,7 +120,8 @@
         bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
             val safeDrawingInsets = insets.safeDrawing
             // Make sure the bottom sheet is not covered by the status bar.
-            bottomSheetContainer.updatePadding(top = safeDrawingInsets.top)
+            bottomSheetBehavior.maxHeight =
+                resources.displayMetrics.heightPixels - safeDrawingInsets.top
             // Make sure the contents inside of the bottom sheet are not hidden by system bars, or
             // cutouts.
             bottomSheet.updatePadding(
@@ -171,7 +191,6 @@
 private val WindowInsets.safeDrawing
     get() =
         getInsets(WindowInsets.Type.systemBars())
-            .union(getInsets(WindowInsets.Type.ime()))
             .union(getInsets(WindowInsets.Type.displayCutout()))
 
 private fun Insets.union(insets: Insets): Insets = Insets.max(this, insets)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 81c2d92..f3a1843 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2380,7 +2380,10 @@
             }
             mCustomMessage = message;
             mKeyguardViewControllerLazy.get().dismissAndCollapse();
-        } else if (callback != null) {
+            return;
+        }
+        Log.w(TAG, "Ignoring request to DISMISS because mShowing=false");
+        if (callback != null) {
             new DismissCallbackWrapper(callback).notifyDismissError();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
index 3cbcb2c..97ea16d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt
@@ -18,21 +18,15 @@
 
 import android.annotation.WorkerThread
 import android.content.ComponentCallbacks2
-import android.graphics.HardwareRenderer
-import android.os.Trace
 import android.util.Log
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
@@ -40,10 +34,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
 /**
@@ -57,37 +48,15 @@
 class ResourceTrimmer
 @Inject
 constructor(
-    private val keyguardInteractor: KeyguardInteractor,
-    private val powerInteractor: PowerInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val globalWindowManager: GlobalWindowManager,
     @Application private val applicationScope: CoroutineScope,
     @Background private val bgDispatcher: CoroutineDispatcher,
-    private val featureFlags: FeatureFlags,
     private val sceneInteractor: SceneInteractor,
-) : CoreStartable, WakefulnessLifecycle.Observer {
+) : CoreStartable {
 
     override fun start() {
         Log.d(LOG_TAG, "Resource trimmer registered.")
-        if (com.android.systemui.Flags.trimResourcesWithBackgroundTrimAtLock()) {
-            applicationScope.launch(bgDispatcher) {
-                // We need to wait for the AoD transition (and animation) to complete.
-                // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f
-                // signal. This is to make sure we don't clear font caches during animation which
-                // would jank and leave stale data in memory.
-                val isDozingFully =
-                    keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged()
-                combine(
-                        powerInteractor.isAsleep,
-                        keyguardInteractor.isDreaming,
-                        isDozingFully,
-                        ::Triple
-                    )
-                    .distinctUntilChanged()
-                    .collect { onWakefulnessUpdated(it.first, it.second, it.third) }
-            }
-        }
-
         applicationScope.launch(bgDispatcher) {
             // We drop 1 to avoid triggering on initial collect().
             if (SceneContainerFlag.isEnabled) {
@@ -110,47 +79,9 @@
         // lockscreen elements, especially clocks.
         Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.")
         globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-        if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) {
-            if (DEBUG) {
-                Log.d(LOG_TAG, "Trimming font caches since keyguard went away.")
-            }
-            globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
-        }
-    }
-
-    @WorkerThread
-    private fun onWakefulnessUpdated(
-        isAsleep: Boolean,
-        isDreaming: Boolean,
-        isDozingFully: Boolean
-    ) {
-        if (!com.android.systemui.Flags.trimResourcesWithBackgroundTrimAtLock()) {
-            return
-        }
-
-        if (DEBUG) {
-            Log.d(LOG_TAG, "isAsleep: $isAsleep Dreaming: $isDreaming DozeAmount: $isDozingFully")
-        }
-        // There are three scenarios:
-        // * No dozing and no AoD at all - where we go directly to ASLEEP with isDreaming = false
-        //      and dozeAmount == 0f
-        // * Dozing without Aod - where we go to ASLEEP with isDreaming = true and dozeAmount jumps
-        //      to 1f
-        // * AoD - where we go to ASLEEP with iDreaming = true and dozeAmount slowly increases
-        //      to 1f
-        val dozeDisabledAndScreenOff = isAsleep && !isDreaming
-        val dozeEnabledAndDozeAnimationCompleted = isAsleep && isDreaming && isDozingFully
-        if (dozeDisabledAndScreenOff || dozeEnabledAndDozeAnimationCompleted) {
-            Trace.beginSection("ResourceTrimmer#trimMemory")
-            Log.d(LOG_TAG, "SysUI asleep, trimming memory.")
-            globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
-            Trace.endSection()
-        }
     }
 
     companion object {
         private const val LOG_TAG = "ResourceTrimmer"
-        private val DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 9b07675f..756c6c2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -57,7 +57,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     private val communalInteractor: CommunalInteractor,
     powerInteractor: PowerInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -70,6 +70,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
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 01109af..2a9ee9f 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
@@ -48,7 +48,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     val deviceEntryRepository: DeviceEntryRepository,
@@ -60,6 +60,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
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 7d3de30..f5e98f1 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
@@ -47,7 +47,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -60,6 +60,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
index 63294f7..47aa02a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
@@ -45,7 +45,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) :
@@ -56,6 +56,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 7961b45..25c3b0d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -51,7 +51,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     private val glanceableHubTransitions: GlanceableHubTransitions,
     powerInteractor: PowerInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -63,6 +63,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index ca6ab3e..e516fa3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -48,7 +48,7 @@
     @Main mainDispatcher: CoroutineDispatcher,
     @Background bgDispatcher: CoroutineDispatcher,
     private val glanceableHubTransitions: GlanceableHubTransitions,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     override val transitionRepository: KeyguardTransitionRepository,
     transitionInteractor: KeyguardTransitionInteractor,
     powerInteractor: PowerInteractor,
@@ -61,6 +61,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
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 8ca29c8..a540d76 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
@@ -49,7 +49,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -64,6 +64,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index f1e98f3..8cab3cd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -58,7 +58,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     private val flags: FeatureFlags,
     private val shadeRepository: ShadeRepository,
     powerInteractor: PowerInteractor,
@@ -73,6 +73,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 2603aab2..86d4cfb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -45,7 +45,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
@@ -57,6 +57,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 76a8223..19b2b81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -52,7 +52,7 @@
     @Background private val scope: CoroutineScope,
     @Background bgDispatcher: CoroutineDispatcher,
     @Main mainDispatcher: CoroutineDispatcher,
-    private val keyguardInteractor: KeyguardInteractor,
+    keyguardInteractor: KeyguardInteractor,
     private val communalInteractor: CommunalInteractor,
     private val flags: FeatureFlags,
     private val keyguardSecurityModel: KeyguardSecurityModel,
@@ -67,6 +67,7 @@
         bgDispatcher = bgDispatcher,
         powerInteractor = powerInteractor,
         keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        keyguardInteractor = keyguardInteractor,
     ) {
 
     override fun start() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index c44a40f..b142b44 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.keyguard.shared.model.DozeStateModel
 import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
 import com.android.systemui.keyguard.shared.model.DozeTransitionModel
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -47,8 +48,10 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
+import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import javax.inject.Provider
@@ -66,7 +69,9 @@
 import kotlinx.coroutines.flow.debounce
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
@@ -96,17 +101,54 @@
     // TODO(b/296118689): move to a repository
     private val _notificationPlaceholderBounds = MutableStateFlow(NotificationContainerBounds())
 
+    // When going to AOD, we interpolate bounds when receiving the new bounds
+    // When going back to LS, we'll apply new bounds directly
+    private val _nonSplitShadeNotifciationPlaceholderBounds =
+        _notificationPlaceholderBounds.pairwise().flatMapLatest { (oldBounds, newBounds) ->
+            val lastChangeStep = keyguardTransitionInteractor.transitionState.first()
+            if (lastChangeStep.to == AOD) {
+                keyguardTransitionInteractor.transitionState.map { step ->
+                    val startingProgress = lastChangeStep.value
+                    val progress = step.value
+                    if (step.to == AOD && progress >= startingProgress) {
+                        val adjustedProgress =
+                            ((progress - startingProgress) / (1F - startingProgress)).coerceIn(
+                                0F,
+                                1F
+                            )
+                        val top = interpolate(oldBounds.top, newBounds.top, adjustedProgress)
+                        val bottom =
+                            interpolate(
+                                oldBounds.bottom,
+                                newBounds.bottom,
+                                adjustedProgress.coerceIn(0F, 1F)
+                            )
+                        NotificationContainerBounds(top = top, bottom = bottom)
+                    } else {
+                        newBounds
+                    }
+                }
+            } else {
+                flow { emit(newBounds) }
+            }
+        }
+
     /** Bounds of the notification container. */
     val notificationContainerBounds: StateFlow<NotificationContainerBounds> by lazy {
         SceneContainerFlag.assertInLegacyMode()
         combine(
                 _notificationPlaceholderBounds,
+                _nonSplitShadeNotifciationPlaceholderBounds,
                 sharedNotificationContainerInteractor.get().configurationBasedDimensions,
-            ) { bounds, cfg ->
+            ) { bounds, nonSplitShadeBounds: NotificationContainerBounds, cfg ->
                 // We offset the placeholder bounds by the configured top margin to account for
                 // legacy placement behavior within notifications for splitshade.
-                if (MigrateClocksToBlueprint.isEnabled && cfg.useSplitShade) {
-                    bounds.copy(bottom = bounds.bottom - cfg.keyguardSplitShadeTopMargin)
+                if (MigrateClocksToBlueprint.isEnabled) {
+                    if (cfg.useSplitShade) {
+                        bounds.copy(bottom = bounds.bottom - cfg.keyguardSplitShadeTopMargin)
+                    } else {
+                        nonSplitShadeBounds
+                    }
                 } else bounds
             }
             .stateIn(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index ccce3bf..8ffa4bb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -105,33 +105,35 @@
         }
 
         return combine(
-            quickAffordanceAlwaysVisible(position),
-            keyguardInteractor.isDozing,
-            if (SceneContainerFlag.isEnabled) {
-                sceneInteractor
-                    .get()
-                    .transitionState
-                    .map {
-                        when (it) {
-                            is ObservableTransitionState.Idle ->
-                                it.currentScene == Scenes.Lockscreen
-                            is ObservableTransitionState.Transition ->
-                                it.fromScene == Scenes.Lockscreen || it.toScene == Scenes.Lockscreen
+                quickAffordanceAlwaysVisible(position),
+                keyguardInteractor.isDozing,
+                if (SceneContainerFlag.isEnabled) {
+                    sceneInteractor
+                        .get()
+                        .transitionState
+                        .map {
+                            when (it) {
+                                is ObservableTransitionState.Idle ->
+                                    it.currentScene == Scenes.Lockscreen
+                                is ObservableTransitionState.Transition ->
+                                    it.fromScene == Scenes.Lockscreen ||
+                                        it.toScene == Scenes.Lockscreen
+                            }
                         }
-                    }
-                    .distinctUntilChanged()
-            } else {
-                keyguardInteractor.isKeyguardShowing
-            },
-            shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(),
-            biometricSettingsRepository.isCurrentUserInLockdown,
-        ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
-            if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) {
-                affordance
-            } else {
-                KeyguardQuickAffordanceModel.Hidden
+                        .distinctUntilChanged()
+                } else {
+                    keyguardInteractor.isKeyguardShowing
+                },
+                shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(),
+                biometricSettingsRepository.isCurrentUserInLockdown,
+            ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown ->
+                if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) {
+                    affordance
+                } else {
+                    KeyguardQuickAffordanceModel.Hidden
+                }
             }
-        }
+            .distinctUntilChanged()
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 323ceef..e148207 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -53,6 +53,7 @@
     val bgDispatcher: CoroutineDispatcher,
     val powerInteractor: PowerInteractor,
     val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    val keyguardInteractor: KeyguardInteractor,
 ) {
     val name = this::class.simpleName ?: "UnknownTransitionInteractor"
     abstract val transitionRepository: KeyguardTransitionRepository
@@ -164,14 +165,10 @@
     @Deprecated("Will be merged into maybeStartTransitionToOccludedOrInsecureCamera")
     suspend fun maybeHandleInsecurePowerGesture(): Boolean {
         if (keyguardOcclusionInteractor.shouldTransitionFromPowerButtonGesture()) {
-            if (transitionInteractor.getCurrentState() == KeyguardState.GONE) {
-                // If the current state is GONE when the launch gesture is triggered, it means we
-                // were in transition from GONE -> DOZING/AOD due to the first power button tap. The
-                // second tap indicates that the user's intent was actually to launch the unlocked
-                // (insecure) camera, so we should transition back to GONE.
+            if (keyguardInteractor.isKeyguardDismissible.value) {
                 startTransitionTo(
                     KeyguardState.GONE,
-                    ownerReason = "Power button gesture while GONE"
+                    ownerReason = "Power button gesture while keyguard is dismissible"
                 )
 
                 return true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
index 6d579f3..f5b82cf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBlueprint.kt
@@ -40,6 +40,7 @@
         rebuildSections: List<KeyguardSection> = listOf(),
         bindData: Boolean = true
     ) {
+        rebuildSections.forEach { it.onRebuildBegin() }
         val prevSections = previousBlueprint?.sections ?: listOf()
         val skipSections = sections.intersect(prevSections).subtract(rebuildSections)
         prevSections.subtract(skipSections).forEach { it.removeViews(constraintLayout) }
@@ -49,6 +50,7 @@
                 it.bindData(constraintLayout)
             }
         }
+        rebuildSections.forEach { it.onRebuildEnd() }
     }
 
     /** Rebuilds views for the target sections, or all of them if unspecified. */
@@ -61,6 +63,7 @@
             return
         }
 
+        rebuildSections.forEach { it.onRebuildBegin() }
         rebuildSections.forEach { it.removeViews(constraintLayout) }
         rebuildSections.forEach {
             it.addViews(constraintLayout)
@@ -68,6 +71,7 @@
                 it.bindData(constraintLayout)
             }
         }
+        rebuildSections.forEach { it.onRebuildEnd() }
     }
 
     fun applyConstraints(constraintSet: ConstraintSet) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSection.kt
index 48a2146..473b08f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSection.kt
@@ -33,6 +33,12 @@
     /** Removes views and does any data binding destruction. */
     abstract fun removeViews(constraintLayout: ConstraintLayout)
 
+    /* Notifies the section is being rebuilt */
+    open fun onRebuildBegin() {}
+
+    /* Notifies the secion that the rebuild is complete */
+    open fun onRebuildEnd() {}
+
     /**
      * Defines equality as same class.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
index 23c2491..807c322 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt
@@ -65,7 +65,7 @@
         val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
         val disposableHandle =
             view.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
                     launch("$TAG#viewModel.alpha") {
                         // Do not independently apply alpha, as [KeyguardRootViewModel] should work
                         // for this and all its children
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index b9a79dc..1cf009d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -30,6 +30,7 @@
 import androidx.core.view.updateLayoutParams
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launch
 import com.android.settingslib.Utils
 import com.android.systemui.animation.Expandable
 import com.android.systemui.animation.view.LaunchableImageView
@@ -80,8 +81,8 @@
         val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
         val disposableHandle =
             view.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    launch("$TAG#viewModel.collect") {
                         viewModel.collect { buttonModel ->
                             updateButton(
                                 view = button,
@@ -93,7 +94,7 @@
                         }
                     }
 
-                    launch {
+                    launch("$TAG#updateButtonAlpha") {
                         updateButtonAlpha(
                             view = button,
                             viewModel = viewModel,
@@ -101,7 +102,7 @@
                         )
                     }
 
-                    launch {
+                    launch("$TAG#configurationBasedDimensions") {
                         configurationBasedDimensions.collect { dimensions ->
                             button.updateLayoutParams<ViewGroup.LayoutParams> {
                                 width = dimensions.buttonSizePx.width
@@ -323,4 +324,6 @@
     private data class ConfigurationBasedDimensions(
         val buttonSizePx: Size,
     )
+
+    private const val TAG = "KeyguardQuickAffordanceViewBinder"
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index c5fab8f..e01f0a1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -147,8 +147,9 @@
                 deviceEntryIconViewModel.get().udfpsLocation.value?.let { udfpsLocation ->
                     Log.d(
                         "DeviceEntrySection",
-                        "udfpsLocation=$udfpsLocation" +
-                            " unusedAuthController=${authController.udfpsLocation}"
+                        "udfpsLocation=$udfpsLocation, " +
+                            "scaledLocation=(${udfpsLocation.centerX},${udfpsLocation.centerY}), " +
+                            "unusedAuthController=${authController.udfpsLocation}"
                     )
                     centerIcon(
                         Point(udfpsLocation.centerX.toInt(), udfpsLocation.centerY.toInt()),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index 2d6690f..0435531 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -56,6 +56,14 @@
     private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null
     private var pastVisibility: Int = -1
 
+    override fun onRebuildBegin() {
+        smartspaceController.suppressDisconnects = true
+    }
+
+    override fun onRebuildEnd() {
+        smartspaceController.suppressDisconnects = false
+    }
+
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (!MigrateClocksToBlueprint.isEnabled) return
         if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 198e9f2..940f423 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -65,7 +65,7 @@
             }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
+                started = SharingStarted.Eagerly,
                 initialValue = ClockSize.LARGE,
             )
 
@@ -74,7 +74,7 @@
             .map { it == ClockSize.LARGE }
             .stateIn(
                 scope = applicationScope,
-                started = SharingStarted.WhileSubscribed(),
+                started = SharingStarted.Eagerly,
                 initialValue = true,
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
index 8409f15..448a71c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt
@@ -23,9 +23,12 @@
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.BurnInModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.res.R
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -42,6 +45,7 @@
     private val burnInInteractor: BurnInInteractor,
     private val shortcutsCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel,
     configurationInteractor: ConfigurationInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) {
 
     /** Notifies when a new configuration is set */
@@ -69,12 +73,22 @@
                 .distinctUntilChanged()
         }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     private val burnIn: Flow<BurnInModel> =
-        burnInInteractor
-            .burnIn(
-                xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
-                yDimenResourceId = R.dimen.default_burn_in_prevention_offset,
-            )
+        combine(
+                burnInInteractor.burnIn(
+                    xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
+                    yDimenResourceId = R.dimen.default_burn_in_prevention_offset,
+                ),
+                keyguardTransitionInteractor.transitionValue(KeyguardState.AOD),
+            ) { burnIn, aodTransitionValue ->
+                BurnInModel(
+                    (burnIn.translationX * aodTransitionValue).toInt(),
+                    (burnIn.translationY * aodTransitionValue).toInt(),
+                    burnIn.scale,
+                    burnIn.scaleClockOnly,
+                )
+            }
             .distinctUntilChanged()
 
     /** An observable for the x-offset by which the indication area should be translated. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
index c4383fc..244d842 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -28,19 +29,23 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class KeyguardQuickAffordancesCombinedViewModel
 @Inject
 constructor(
+    @Application applicationScope: CoroutineScope,
     private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
     private val keyguardInteractor: KeyguardInteractor,
     shadeInteractor: ShadeInteractor,
@@ -84,15 +89,20 @@
     /** The only time the expansion is important is while lockscreen is actively displayed */
     private val shadeExpansionAlpha =
         combine(
-            showingLockscreen,
-            shadeInteractor.anyExpansion,
-        ) { showingLockscreen, expansion ->
-            if (showingLockscreen) {
-                1 - expansion
-            } else {
-                0f
+                showingLockscreen,
+                shadeInteractor.anyExpansion,
+            ) { showingLockscreen, expansion ->
+                if (showingLockscreen) {
+                    1 - expansion
+                } else {
+                    0f
+                }
             }
-        }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Lazily,
+                initialValue = 0f,
+            )
 
     /**
      * ID of the slot that's currently selected in the preview that renders exclusively in the
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
index e0c5419..9c29bab 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt
@@ -43,6 +43,7 @@
     @IntoMap
     @ClassKey(MediaDataProcessor::class)
     fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable
+
     companion object {
 
         @Provides
@@ -52,7 +53,7 @@
             newProvider: Provider<MediaCarouselInteractor>,
             mediaFlags: MediaFlags,
         ): MediaDataManager {
-            return if (mediaFlags.isMediaControlsRefactorEnabled()) {
+            return if (mediaFlags.isSceneContainerEnabled()) {
                 newProvider.get()
             } else {
                 legacyProvider.get()
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index eed7752..8e985e1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -269,7 +269,7 @@
         }
 
     override fun start() {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             return
         }
 
@@ -746,8 +746,7 @@
             notif.extras.getParcelable(
                 Notification.EXTRA_BUILDER_APPLICATION_INFO,
                 ApplicationInfo::class.java
-            )
-                ?: getAppInfoFromPackage(sbn.packageName)
+            ) ?: getAppInfoFromPackage(sbn.packageName)
 
         // App name
         val appName = getAppName(sbn, appInfo)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
index 9e62300..b4bd4fd 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt
@@ -36,8 +36,8 @@
 import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.shared.model.MediaCommonModel
-import com.android.systemui.media.controls.util.MediaControlsRefactorFlag
 import com.android.systemui.media.controls.util.MediaFlags
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import java.io.PrintWriter
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -127,7 +127,7 @@
     val currentMedia: StateFlow<List<MediaCommonModel>> = mediaFilterRepository.currentMedia
 
     override fun start() {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             return
         }
 
@@ -256,8 +256,6 @@
     companion object {
         val unsupported: Nothing
             get() =
-                error(
-                    "Code path not supported when ${MediaControlsRefactorFlag.FLAG_NAME} is enabled"
-                )
+                error("Code path not supported when ${SceneContainerFlag.DESCRIPTION} is enabled")
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index 19e3e07..7bacdeb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -42,6 +42,7 @@
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Dumpable
 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.dump.DumpManager
@@ -123,6 +124,7 @@
 class MediaCarouselController
 @Inject
 constructor(
+    @Application applicationScope: CoroutineScope,
     private val context: Context,
     private val mediaControlPanelFactory: Provider<MediaControlPanel>,
     private val visualStabilityProvider: VisualStabilityProvider,
@@ -217,7 +219,7 @@
     private val animationScaleObserver: ContentObserver =
         object : ContentObserver(null) {
             override fun onChange(selfChange: Boolean) {
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+                if (!mediaFlags.isSceneContainerEnabled()) {
                     MediaPlayerData.players().forEach { it.updateAnimatorDurationScale() }
                 } else {
                     controllerByViewModel.values.forEach { it.updateAnimatorDurationScale() }
@@ -347,7 +349,7 @@
         inflateSettingsButton()
         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
         configurationController.addCallback(configListener)
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             setUpListeners()
         } else {
             val visualStabilityCallback = OnReorderingAllowedListener {
@@ -387,12 +389,12 @@
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 listenForAnyStateToGoneKeyguardTransition(this)
                 listenForAnyStateToLockscreenTransition(this)
-                listenForLockscreenSettingChanges(this)
 
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) return@repeatOnLifecycle
+                if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle
                 listenForMediaItemsChanges(this)
             }
         }
+        listenForLockscreenSettingChanges(applicationScope)
 
         // Notifies all active players about animation scale changes.
         globalSettings.registerContentObserver(
@@ -882,8 +884,7 @@
                     val previousVisibleIndex =
                         MediaPlayerData.playerKeys().indexOfFirst { key -> it == key }
                     mediaCarouselScrollHandler.scrollToPlayer(previousVisibleIndex, mediaIndex)
-                }
-                    ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
+                } ?: mediaCarouselScrollHandler.scrollToPlayer(destIndex = mediaIndex)
             }
         } else if (isRtl && mediaContent.childCount > 0) {
             // In RTL, Scroll to the first player as it is the rightmost player in media carousel.
@@ -1092,7 +1093,7 @@
     }
 
     private fun updatePlayers(recreateMedia: Boolean) {
-        if (mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (mediaFlags.isSceneContainerEnabled()) {
             updateMediaPlayers(recreateMedia)
             return
         }
@@ -1192,7 +1193,7 @@
             currentStartLocation = startLocation
             currentEndLocation = endLocation
             currentTransitionProgress = progress
-            if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+            if (!mediaFlags.isSceneContainerEnabled()) {
                 for (mediaPlayer in MediaPlayerData.players()) {
                     updateViewControllerToState(mediaPlayer.mediaViewController, immediately)
                 }
@@ -1254,7 +1255,7 @@
 
     /** Update listening to seekbar. */
     private fun updateSeekbarListening(visibleToUser: Boolean) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             for (player in MediaPlayerData.players()) {
                 player.setListening(visibleToUser && currentlyExpanded)
             }
@@ -1269,7 +1270,7 @@
     private fun updateCarouselDimensions() {
         var width = 0
         var height = 0
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             for (mediaPlayer in MediaPlayerData.players()) {
                 val controller = mediaPlayer.mediaViewController
                 // When transitioning the view to gone, the view gets smaller, but the translation
@@ -1361,7 +1362,7 @@
                         !mediaManager.hasActiveMediaOrRecommendation() &&
                         desiredHostState.showsOnlyActiveMedia
 
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+                if (!mediaFlags.isSceneContainerEnabled()) {
                     for (mediaPlayer in MediaPlayerData.players()) {
                         if (animate) {
                             mediaPlayer.mediaViewController.animatePendingStateChange(
@@ -1401,7 +1402,7 @@
         }
 
     fun closeGuts(immediate: Boolean = true) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             MediaPlayerData.players().forEach { it.closeGuts(immediate) }
         } else {
             controllerByViewModel.values.forEach { it.closeGuts(immediate) }
@@ -1544,7 +1545,7 @@
 
     @VisibleForTesting
     fun onSwipeToDismiss() {
-        if (mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (mediaFlags.isSceneContainerEnabled()) {
             mediaCarouselViewModel.onSwipeToDismiss()
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt
index a4f3e21..6589038 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.util.MediaFlags
@@ -61,6 +62,11 @@
 import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.launch
 
 private val TAG: String = MediaHierarchyManager::class.java.simpleName
@@ -89,6 +95,7 @@
  * This manager is responsible for placement of the unique media view between the different hosts
  * and animate the positions of the views to achieve seamless transitions.
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class MediaHierarchyManager
 @Inject
@@ -101,6 +108,7 @@
     private val mediaManager: MediaDataManager,
     private val keyguardViewController: KeyguardViewController,
     private val dreamOverlayStateController: DreamOverlayStateController,
+    private val keyguardInteractor: KeyguardInteractor,
     communalTransitionViewModel: CommunalTransitionViewModel,
     configurationController: ConfigurationController,
     wakefulnessLifecycle: WakefulnessLifecycle,
@@ -236,6 +244,15 @@
 
     private var inSplitShade = false
 
+    /**
+     * Whether we are transitioning to the hub or from the hub to the shade. If so, use fade as the
+     * transformation type and skip calculating state with the bounds and the transition progress.
+     */
+    private val isHubTransition
+        get() =
+            desiredLocation == LOCATION_COMMUNAL_HUB ||
+                (previousLocation == LOCATION_COMMUNAL_HUB && desiredLocation == LOCATION_QS)
+
     /** Is there any active media or recommendation in the carousel? */
     private var hasActiveMediaOrRecommendation: Boolean = false
         get() = mediaManager.hasActiveMediaOrRecommendation()
@@ -413,6 +430,12 @@
     /** Is the communal UI showing */
     private var isCommunalShowing: Boolean = false
 
+    /** Is the communal UI showing and not dreaming */
+    private var onCommunalNotDreaming: Boolean = false
+
+    /** Is the communal UI showing, dreaming and shade expanding */
+    private var onCommunalDreamingAndShadeExpanding: Boolean = false
+
     /**
      * The current cross fade progress. 0.5f means it's just switching between the start and the end
      * location and the content is fully faded, while 0.75f means that we're halfway faded in again
@@ -585,11 +608,26 @@
 
         // Listen to the communal UI state. Make sure that communal UI is showing and hub itself is
         // available, ie. not disabled and able to be shown.
+        // When dreaming, qs expansion is immediately set to 1f, so we listen to shade expansion to
+        // calculate the new location.
         coroutineScope.launch {
-            communalTransitionViewModel.isUmoOnCommunal.collect { value ->
-                isCommunalShowing = value
-                updateDesiredLocation(forceNoAnimation = true)
-            }
+            combine(
+                    communalTransitionViewModel.isUmoOnCommunal,
+                    keyguardInteractor.isDreaming,
+                    // keep on communal before the shade is expanded enough to show the elements in
+                    // QS
+                    shadeInteractor.shadeExpansion
+                        .mapLatest { it < EXPANSION_THRESHOLD }
+                        .distinctUntilChanged(),
+                    ::Triple
+                )
+                .collectLatest { (communalShowing, isDreaming, isShadeExpanding) ->
+                    isCommunalShowing = communalShowing
+                    onCommunalDreamingAndShadeExpanding =
+                        communalShowing && isDreaming && isShadeExpanding
+                    onCommunalNotDreaming = communalShowing && !isDreaming
+                    updateDesiredLocation(forceNoAnimation = true)
+                }
         }
     }
 
@@ -805,6 +843,9 @@
         if (skipQqsOnExpansion) {
             return false
         }
+        if (isHubTransition) {
+            return false
+        }
         // This is an invalid transition, and can happen when using the camera gesture from the
         // lock screen. Disallow.
         if (
@@ -947,6 +988,9 @@
     @VisibleForTesting
     @TransformationType
     fun calculateTransformationType(): Int {
+        if (isHubTransition) {
+            return TRANSFORMATION_TYPE_FADE
+        }
         if (isTransitioningToFullShade) {
             if (inSplitShade && areGuidedTransitionHostsVisible()) {
                 return TRANSFORMATION_TYPE_TRANSITION
@@ -977,7 +1021,7 @@
      *   otherwise
      */
     private fun getTransformationProgress(): Float {
-        if (skipQqsOnExpansion) {
+        if (skipQqsOnExpansion || isHubTransition) {
             return -1.0f
         }
         val progress = getQSTransformationProgress()
@@ -1147,15 +1191,18 @@
         }
         val onLockscreen =
             (!bypassController.bypassEnabled && (statusbarState == StatusBarState.KEYGUARD))
+
+        // UMO should show on hub unless the qs is expanding when not dreaming, or shade is
+        // expanding when dreaming
+        val onCommunal =
+            (onCommunalNotDreaming && qsExpansion == 0.0f) || onCommunalDreamingAndShadeExpanding
         val location =
             when {
                 mediaFlags.isSceneContainerEnabled() -> desiredLocation
                 dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY
-
-                // UMO should show in communal unless the shade is expanding or visible.
-                isCommunalShowing && qsExpansion == 0.0f -> LOCATION_COMMUNAL_HUB
+                onCommunal -> LOCATION_COMMUNAL_HUB
                 (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
-                qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
+                qsExpansion > EXPANSION_THRESHOLD && onLockscreen -> LOCATION_QS
                 onLockscreen && isSplitShadeExpanding() -> LOCATION_QS
                 onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS
 
@@ -1190,6 +1237,9 @@
             // reattach it without an animation
             return LOCATION_LOCKSCREEN
         }
+        // When communal showing while dreaming, skipQqsOnExpansion is also true but we want to
+        // return the calculated location, so it won't disappear as soon as shade is pulled down.
+        if (isCommunalShowing) return location
         if (skipQqsOnExpansion) {
             // When doing an immediate expand or collapse, we want to keep it in QS.
             return LOCATION_QS
@@ -1288,6 +1338,9 @@
          * transitioning
          */
         const val TRANSFORMATION_TYPE_FADE = 1
+
+        /** Expansion amount value at which elements start to become visible in the QS panel. */
+        const val EXPANSION_THRESHOLD = 0.4f
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index 3837708..9d07232 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -203,7 +203,7 @@
     private val scrubbingChangeListener =
         object : SeekBarViewModel.ScrubbingChangeListener {
             override fun onScrubbingChanged(scrubbing: Boolean) {
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+                if (!mediaFlags.isSceneContainerEnabled()) return
                 if (isScrubbing == scrubbing) return
                 isScrubbing = scrubbing
                 updateDisplayForScrubbingChange()
@@ -213,7 +213,7 @@
     private val enabledChangeListener =
         object : SeekBarViewModel.EnabledChangeListener {
             override fun onEnabledChanged(enabled: Boolean) {
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+                if (!mediaFlags.isSceneContainerEnabled()) return
                 if (isSeekBarEnabled == enabled) return
                 isSeekBarEnabled = enabled
                 MediaControlViewBinder.updateSeekBarVisibility(expandedLayout, isSeekBarEnabled)
@@ -229,7 +229,7 @@
      * @param listening True when player should be active. Otherwise, false.
      */
     fun setListening(listening: Boolean) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         seekBarViewModel.listening = listening
     }
 
@@ -263,7 +263,7 @@
                             )
                         )
                     }
-                    if (mediaFlags.isMediaControlsRefactorEnabled()) {
+                    if (mediaFlags.isSceneContainerEnabled()) {
                         if (
                             this@MediaViewController::recsConfigurationChangeListener.isInitialized
                         ) {
@@ -305,6 +305,7 @@
      */
     var collapsedLayout = ConstraintSet()
         @VisibleForTesting set
+
     /**
      * The expanded constraint set used to render a collapsed player. If it is modified, make sure
      * to call [refreshState]
@@ -334,7 +335,7 @@
      * Notify this controller that the view has been removed and all listeners should be destroyed
      */
     fun onDestroy() {
-        if (mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (mediaFlags.isSceneContainerEnabled()) {
             if (this::seekBarObserver.isInitialized) {
                 seekBarViewModel.progress.removeObserver(seekBarObserver)
             }
@@ -657,7 +658,7 @@
         }
 
     fun attachPlayer(mediaViewHolder: MediaViewHolder) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         this.mediaViewHolder = mediaViewHolder
 
         // Setting up seek bar.
@@ -731,7 +732,7 @@
     }
 
     fun updateAnimatorDurationScale() {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         if (this::seekBarObserver.isInitialized) {
             seekBarObserver.animationEnabled =
                 globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f
@@ -787,7 +788,7 @@
     }
 
     fun attachRecommendations(recommendationViewHolder: RecommendationViewHolder) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         this.recommendationViewHolder = recommendationViewHolder
 
         attach(recommendationViewHolder.recommendations, TYPE.RECOMMENDATION)
@@ -796,13 +797,13 @@
     }
 
     fun bindSeekBar(onSeek: () -> Unit, onBindSeekBar: (SeekBarViewModel) -> Unit) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         seekBarViewModel.logSeek = onSeek
         onBindSeekBar.invoke(seekBarViewModel)
     }
 
     fun setUpTurbulenceNoise() {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         if (!this::turbulenceNoiseAnimationConfig.isInitialized) {
             turbulenceNoiseAnimationConfig =
                 createTurbulenceNoiseConfig(
@@ -1153,13 +1154,13 @@
     }
 
     fun setUpPrevButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         isPrevButtonAvailable = isAvailable
         prevNotVisibleValue = notVisibleValue
     }
 
     fun setUpNextButtonInfo(isAvailable: Boolean, notVisibleValue: Int = ConstraintSet.GONE) {
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) return
+        if (!mediaFlags.isSceneContainerEnabled()) return
         isNextButtonAvailable = isAvailable
         nextNotVisibleValue = notVisibleValue
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt
deleted file mode 100644
index 2850b4b..0000000
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaControlsRefactorFlag.kt
+++ /dev/null
@@ -1,53 +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.systemui.media.controls.util
-
-import com.android.systemui.Flags
-import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
-
-/** Helper for reading or using the media_controls_refactor flag state. */
-@Suppress("NOTHING_TO_INLINE")
-object MediaControlsRefactorFlag {
-    /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_MEDIA_CONTROLS_REFACTOR
-
-    /** A token used for dependency declaration */
-    val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
-
-    /** Is the flag enabled? */
-    @JvmStatic
-    inline val isEnabled
-        get() = Flags.mediaControlsRefactor()
-
-    /**
-     * Called to ensure code is only run when the flag is enabled. This protects users from the
-     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
-     * build to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun isUnexpectedlyInLegacyMode() =
-        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-    /**
-     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
-     * the flag is enabled to ensure that the refactor author catches issues in testing.
-     */
-    @JvmStatic
-    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
index 1e7bc0c..21c3111 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt
@@ -52,8 +52,4 @@
 
     /** Check whether to use scene framework */
     fun isSceneContainerEnabled() = SceneContainerFlag.isEnabled
-
-    /** Check whether to use media refactor code */
-    fun isMediaControlsRefactorEnabled() =
-        MediaControlsRefactorFlag.isEnabled && SceneContainerFlag.isEnabled
 }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
index a256b59..e931f8f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -18,10 +18,7 @@
 private const val TAG = "BackPanel"
 private const val DEBUG = false
 
-class BackPanel(
-        context: Context,
-        private val latencyTracker: LatencyTracker
-) : View(context) {
+class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
 
     var arrowsPointLeft = false
         set(value) {
@@ -42,39 +39,39 @@
     // True if the panel is currently on the left of the screen
     var isLeftPanel = false
 
-    /**
-     * Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw]
-     */
+    /** Used to track back arrow latency from [android.view.MotionEvent.ACTION_DOWN] to [onDraw] */
     private var trackingBackArrowLatency = false
 
-    /**
-     * The length of the arrow measured horizontally. Used for animating [arrowPath]
-     */
-    private var arrowLength = AnimatedFloat(
+    /** The length of the arrow measured horizontally. Used for animating [arrowPath] */
+    private var arrowLength =
+        AnimatedFloat(
             name = "arrowLength",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
-    )
+        )
 
     /**
      * The height of the arrow measured vertically from its center to its top (i.e. half the total
      * height). Used for animating [arrowPath]
      */
-    var arrowHeight = AnimatedFloat(
+    var arrowHeight =
+        AnimatedFloat(
             name = "arrowHeight",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
-    )
+        )
 
-    val backgroundWidth = AnimatedFloat(
+    val backgroundWidth =
+        AnimatedFloat(
             name = "backgroundWidth",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
             minimumValue = 0f,
-    )
+        )
 
-    val backgroundHeight = AnimatedFloat(
+    val backgroundHeight =
+        AnimatedFloat(
             name = "backgroundHeight",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
             minimumValue = 0f,
-    )
+        )
 
     /**
      * Corners of the background closer to the edge of the screen (where the arrow appeared from).
@@ -88,17 +85,19 @@
      */
     val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
 
-    var scale = AnimatedFloat(
+    var scale =
+        AnimatedFloat(
             name = "scale",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
             minimumValue = 0f
-    )
+        )
 
-    val scalePivotX = AnimatedFloat(
+    val scalePivotX =
+        AnimatedFloat(
             name = "scalePivotX",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
             minimumValue = backgroundWidth.pos / 2,
-    )
+        )
 
     /**
      * Left/right position of the background relative to the canvas. Also corresponds with the
@@ -107,21 +106,24 @@
      */
     var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
 
-    var arrowAlpha = AnimatedFloat(
+    var arrowAlpha =
+        AnimatedFloat(
             name = "arrowAlpha",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
             minimumValue = 0f,
             maximumValue = 1f
-    )
+        )
 
-    val backgroundAlpha = AnimatedFloat(
+    val backgroundAlpha =
+        AnimatedFloat(
             name = "backgroundAlpha",
             minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
             minimumValue = 0f,
             maximumValue = 1f
-    )
+        )
 
-    private val allAnimatedFloat = setOf(
+    private val allAnimatedFloat =
+        setOf(
             arrowLength,
             arrowHeight,
             backgroundWidth,
@@ -132,7 +134,7 @@
             horizontalTranslation,
             arrowAlpha,
             backgroundAlpha
-    )
+        )
 
     /**
      * Canvas vertical translation. How far up/down the arrow and background appear relative to the
@@ -140,43 +142,45 @@
      */
     var verticalTranslation = AnimatedFloat("verticalTranslation")
 
-    /**
-     * Use for drawing debug info. Can only be set if [DEBUG]=true
-     */
+    /** Use for drawing debug info. Can only be set if [DEBUG]=true */
     var drawDebugInfo: ((canvas: Canvas) -> Unit)? = null
         set(value) {
             if (DEBUG) field = value
         }
 
     internal fun updateArrowPaint(arrowThickness: Float) {
-
         arrowPaint.strokeWidth = arrowThickness
 
-        val isDeviceInNightTheme = resources.configuration.uiMode and
-                Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+        val isDeviceInNightTheme =
+            resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+                Configuration.UI_MODE_NIGHT_YES
 
-        arrowPaint.color = Utils.getColorAttrDefaultColor(context,
+        arrowPaint.color =
+            Utils.getColorAttrDefaultColor(
+                context,
                 if (isDeviceInNightTheme) {
                     com.android.internal.R.attr.materialColorOnSecondaryContainer
                 } else {
                     com.android.internal.R.attr.materialColorOnSecondaryFixed
                 }
-        )
+            )
 
-        arrowBackgroundPaint.color = Utils.getColorAttrDefaultColor(context,
+        arrowBackgroundPaint.color =
+            Utils.getColorAttrDefaultColor(
+                context,
                 if (isDeviceInNightTheme) {
                     com.android.internal.R.attr.materialColorSecondaryContainer
                 } else {
                     com.android.internal.R.attr.materialColorSecondaryFixedDim
                 }
-        )
+            )
     }
 
     inner class AnimatedFloat(
-            name: String,
-            private val minimumVisibleChange: Float? = null,
-            private val minimumValue: Float? = null,
-            private val maximumValue: Float? = null,
+        name: String,
+        private val minimumVisibleChange: Float? = null,
+        private val minimumValue: Float? = null,
+        private val maximumValue: Float? = null,
     ) {
 
         // The resting position when not stretched by a touch drag
@@ -207,19 +211,21 @@
         }
 
         init {
-            val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
-                override fun setValue(animatedFloat: AnimatedFloat, value: Float) {
-                    animatedFloat.pos = value
-                }
+            val floatProp =
+                object : FloatPropertyCompat<AnimatedFloat>(name) {
+                    override fun setValue(animatedFloat: AnimatedFloat, value: Float) {
+                        animatedFloat.pos = value
+                    }
 
-                override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
-            }
-            animation = SpringAnimation(this, floatProp).apply {
-                spring = SpringForce()
-                this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
-                this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
-                this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
-            }
+                    override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
+                }
+            animation =
+                SpringAnimation(this, floatProp).apply {
+                    spring = SpringForce()
+                    this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
+                    this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
+                    this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
+                }
         }
 
         fun snapTo(newPosition: Float) {
@@ -233,11 +239,10 @@
             snapTo(restingPosition)
         }
 
-
         fun stretchTo(
-                stretchAmount: Float,
-                startingVelocity: Float? = null,
-                springForce: SpringForce? = null
+            stretchAmount: Float,
+            startingVelocity: Float? = null,
+            springForce: SpringForce? = null
         ) {
             animation.apply {
                 startingVelocity?.let {
@@ -297,8 +302,8 @@
     }
 
     fun addAnimationEndListener(
-            animatedFloat: AnimatedFloat,
-            endListener: DelayedOnAnimationEndListener
+        animatedFloat: AnimatedFloat,
+        endListener: DelayedOnAnimationEndListener
     ): Boolean {
         return if (animatedFloat.isRunning) {
             animatedFloat.addEndListener(endListener)
@@ -314,51 +319,51 @@
     }
 
     fun setStretch(
-            horizontalTranslationStretchAmount: Float,
-            arrowStretchAmount: Float,
-            arrowAlphaStretchAmount: Float,
-            backgroundAlphaStretchAmount: Float,
-            backgroundWidthStretchAmount: Float,
-            backgroundHeightStretchAmount: Float,
-            edgeCornerStretchAmount: Float,
-            farCornerStretchAmount: Float,
-            fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
+        horizontalTranslationStretchAmount: Float,
+        arrowStretchAmount: Float,
+        arrowAlphaStretchAmount: Float,
+        backgroundAlphaStretchAmount: Float,
+        backgroundWidthStretchAmount: Float,
+        backgroundHeightStretchAmount: Float,
+        edgeCornerStretchAmount: Float,
+        farCornerStretchAmount: Float,
+        fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
     ) {
         horizontalTranslation.stretchBy(
-                finalPosition = fullyStretchedDimens.horizontalTranslation,
-                amount = horizontalTranslationStretchAmount
+            finalPosition = fullyStretchedDimens.horizontalTranslation,
+            amount = horizontalTranslationStretchAmount
         )
         arrowLength.stretchBy(
-                finalPosition = fullyStretchedDimens.arrowDimens.length,
-                amount = arrowStretchAmount
+            finalPosition = fullyStretchedDimens.arrowDimens.length,
+            amount = arrowStretchAmount
         )
         arrowHeight.stretchBy(
-                finalPosition = fullyStretchedDimens.arrowDimens.height,
-                amount = arrowStretchAmount
+            finalPosition = fullyStretchedDimens.arrowDimens.height,
+            amount = arrowStretchAmount
         )
         arrowAlpha.stretchBy(
-                finalPosition = fullyStretchedDimens.arrowDimens.alpha,
-                amount = arrowAlphaStretchAmount
+            finalPosition = fullyStretchedDimens.arrowDimens.alpha,
+            amount = arrowAlphaStretchAmount
         )
         backgroundAlpha.stretchBy(
-                finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
-                amount = backgroundAlphaStretchAmount
+            finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
+            amount = backgroundAlphaStretchAmount
         )
         backgroundWidth.stretchBy(
-                finalPosition = fullyStretchedDimens.backgroundDimens.width,
-                amount = backgroundWidthStretchAmount
+            finalPosition = fullyStretchedDimens.backgroundDimens.width,
+            amount = backgroundWidthStretchAmount
         )
         backgroundHeight.stretchBy(
-                finalPosition = fullyStretchedDimens.backgroundDimens.height,
-                amount = backgroundHeightStretchAmount
+            finalPosition = fullyStretchedDimens.backgroundDimens.height,
+            amount = backgroundHeightStretchAmount
         )
         backgroundEdgeCornerRadius.stretchBy(
-                finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
-                amount = edgeCornerStretchAmount
+            finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
+            amount = edgeCornerStretchAmount
         )
         backgroundFarCornerRadius.stretchBy(
-                finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
-                amount = farCornerStretchAmount
+            finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
+            amount = farCornerStretchAmount
         )
     }
 
@@ -373,8 +378,11 @@
     }
 
     fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
-        arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity,
-                springForce = springForce)
+        arrowAlpha.stretchTo(
+            stretchAmount = 0f,
+            startingVelocity = startingVelocity,
+            springForce = springForce
+        )
     }
 
     fun resetStretch() {
@@ -392,12 +400,10 @@
         backgroundFarCornerRadius.snapToRestingPosition()
     }
 
-    /**
-     * Updates resting arrow and background size not accounting for stretch
-     */
+    /** Updates resting arrow and background size not accounting for stretch */
     internal fun setRestingDimens(
-            restingParams: EdgePanelParams.BackIndicatorDimens,
-            animate: Boolean = true
+        restingParams: EdgePanelParams.BackIndicatorDimens,
+        animate: Boolean = true
     ) {
         horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
         scale.updateRestingPosition(restingParams.scale)
@@ -410,27 +416,29 @@
         backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
         backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
         backgroundEdgeCornerRadius.updateRestingPosition(
-                restingParams.backgroundDimens.edgeCornerRadius, animate
+            restingParams.backgroundDimens.edgeCornerRadius,
+            animate
         )
         backgroundFarCornerRadius.updateRestingPosition(
-                restingParams.backgroundDimens.farCornerRadius, animate
+            restingParams.backgroundDimens.farCornerRadius,
+            animate
         )
     }
 
     fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
 
     fun setSpring(
-            horizontalTranslation: SpringForce? = null,
-            verticalTranslation: SpringForce? = null,
-            scale: SpringForce? = null,
-            arrowLength: SpringForce? = null,
-            arrowHeight: SpringForce? = null,
-            arrowAlpha: SpringForce? = null,
-            backgroundAlpha: SpringForce? = null,
-            backgroundFarCornerRadius: SpringForce? = null,
-            backgroundEdgeCornerRadius: SpringForce? = null,
-            backgroundWidth: SpringForce? = null,
-            backgroundHeight: SpringForce? = null,
+        horizontalTranslation: SpringForce? = null,
+        verticalTranslation: SpringForce? = null,
+        scale: SpringForce? = null,
+        arrowLength: SpringForce? = null,
+        arrowHeight: SpringForce? = null,
+        arrowAlpha: SpringForce? = null,
+        backgroundAlpha: SpringForce? = null,
+        backgroundFarCornerRadius: SpringForce? = null,
+        backgroundEdgeCornerRadius: SpringForce? = null,
+        backgroundWidth: SpringForce? = null,
+        backgroundHeight: SpringForce? = null,
     ) {
         arrowLength?.let { this.arrowLength.spring = it }
         arrowHeight?.let { this.arrowHeight.spring = it }
@@ -459,26 +467,28 @@
 
         if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
 
-        canvas.translate(
-                horizontalTranslation.pos,
-                height * 0.5f + verticalTranslation.pos
-        )
+        canvas.translate(horizontalTranslation.pos, height * 0.5f + verticalTranslation.pos)
 
         canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
 
-        val arrowBackground = arrowBackgroundRect.apply {
-            left = 0f
-            top = -halfHeight
-            right = backgroundWidth
-            bottom = halfHeight
-        }.toPathWithRoundCorners(
-                topLeft = edgeCorner,
-                bottomLeft = edgeCorner,
-                topRight = farCorner,
-                bottomRight = farCorner
+        val arrowBackground =
+            arrowBackgroundRect
+                .apply {
+                    left = 0f
+                    top = -halfHeight
+                    right = backgroundWidth
+                    bottom = halfHeight
+                }
+                .toPathWithRoundCorners(
+                    topLeft = edgeCorner,
+                    bottomLeft = edgeCorner,
+                    topRight = farCorner,
+                    bottomRight = farCorner
+                )
+        canvas.drawPath(
+            arrowBackground,
+            arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() }
         )
-        canvas.drawPath(arrowBackground,
-                arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() })
 
         val dx = arrowLength.pos
         val dy = arrowHeight.pos
@@ -487,8 +497,8 @@
         // either the tip or the back of the arrow, whichever is closer
         val arrowOffset = (backgroundWidth - dx) / 2
         canvas.translate(
-                /* dx= */ arrowOffset,
-                /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+            /* dx= */ arrowOffset,
+            /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
         )
 
         val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
@@ -500,8 +510,8 @@
         }
 
         val arrowPath = calculateArrowPath(dx = dx, dy = dy)
-        val arrowPaint = arrowPaint
-                .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
+        val arrowPaint =
+            arrowPaint.apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
         canvas.drawPath(arrowPath, arrowPaint)
         canvas.restore()
 
@@ -519,17 +529,23 @@
     }
 
     private fun RectF.toPathWithRoundCorners(
-            topLeft: Float = 0f,
-            topRight: Float = 0f,
-            bottomRight: Float = 0f,
-            bottomLeft: Float = 0f
-    ): Path = Path().apply {
-        val corners = floatArrayOf(
-                topLeft, topLeft,
-                topRight, topRight,
-                bottomRight, bottomRight,
-                bottomLeft, bottomLeft
-        )
-        addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
-    }
-}
\ No newline at end of file
+        topLeft: Float = 0f,
+        topRight: Float = 0f,
+        bottomRight: Float = 0f,
+        bottomLeft: Float = 0f
+    ): Path =
+        Path().apply {
+            val corners =
+                floatArrayOf(
+                    topLeft,
+                    topLeft,
+                    topRight,
+                    topRight,
+                    bottomRight,
+                    bottomRight,
+                    bottomLeft,
+                    bottomLeft
+                )
+            addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index f8086f5..18358a7 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -27,7 +27,6 @@
 import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import android.view.VelocityTracker
-import android.view.View
 import android.view.ViewConfiguration
 import android.view.WindowManager
 import androidx.annotation.VisibleForTesting
@@ -37,11 +36,12 @@
 import com.android.internal.jank.Cuj
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.util.LatencyTracker
-import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.NavigationEdgeBackPlugin
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.ViewController
+import com.android.systemui.util.concurrency.BackPanelUiThread
+import com.android.systemui.util.concurrency.UiThreadContext
 import com.android.systemui.util.time.SystemClock
 import java.io.PrintWriter
 import javax.inject.Inject
@@ -85,11 +85,11 @@
     context: Context,
     private val windowManager: WindowManager,
     private val viewConfiguration: ViewConfiguration,
-    @Main private val mainHandler: Handler,
+    private val mainHandler: Handler,
     private val systemClock: SystemClock,
     private val vibratorHelper: VibratorHelper,
     private val configurationController: ConfigurationController,
-    private val latencyTracker: LatencyTracker,
+    latencyTracker: LatencyTracker,
     private val interactionJankMonitor: InteractionJankMonitor,
 ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
 
@@ -104,7 +104,7 @@
     constructor(
         private val windowManager: WindowManager,
         private val viewConfiguration: ViewConfiguration,
-        @Main private val mainHandler: Handler,
+        @BackPanelUiThread private val uiThreadContext: UiThreadContext,
         private val systemClock: SystemClock,
         private val vibratorHelper: VibratorHelper,
         private val configurationController: ConfigurationController,
@@ -113,20 +113,19 @@
     ) {
         /** Construct a [BackPanelController]. */
         fun create(context: Context): BackPanelController {
-            val backPanelController =
-                BackPanelController(
+            uiThreadContext.isCurrentThread()
+            return BackPanelController(
                     context,
                     windowManager,
                     viewConfiguration,
-                    mainHandler,
+                    uiThreadContext.handler,
                     systemClock,
                     vibratorHelper,
                     configurationController,
                     latencyTracker,
                     interactionJankMonitor
                 )
-            backPanelController.init()
-            return backPanelController
+                .also { it.init() }
         }
     }
 
@@ -164,6 +163,7 @@
 
     private val elapsedTimeSinceInactive
         get() = systemClock.uptimeMillis() - gestureInactiveTime
+
     private val elapsedTimeSinceEntry
         get() = systemClock.uptimeMillis() - gestureEntryTime
 
@@ -612,6 +612,7 @@
     }
 
     private var previousPreThresholdWidthInterpolator = params.entryWidthInterpolator
+
     private fun preThresholdWidthStretchAmount(progress: Float): Float {
         val interpolator = run {
             val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
@@ -677,8 +678,7 @@
             velocityTracker?.run {
                 computeCurrentVelocity(PX_PER_SEC)
                 xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
-            }
-                ?: 0f
+            } ?: 0f
         val isPastFlingVelocityThreshold =
             flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
         return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
@@ -1006,15 +1006,15 @@
 
     private fun performDeactivatedHapticFeedback() {
         vibratorHelper.performHapticFeedback(
-                mView,
-                HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
+            mView,
+            HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
         )
     }
 
     private fun performActivatedHapticFeedback() {
         vibratorHelper.performHapticFeedback(
-                mView,
-                HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
+            mView,
+            HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
         )
     }
 
@@ -1028,8 +1028,7 @@
             velocityTracker?.run {
                 computeCurrentVelocity(PX_PER_MS)
                 MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
-            }
-                ?: valueOnFastVelocity
+            } ?: valueOnFastVelocity
 
         return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
     }
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 d0f8412..41cd2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -44,8 +44,6 @@
 import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.icu.text.SimpleDateFormat;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -55,7 +53,6 @@
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
-import android.view.Choreographer;
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
 import android.view.InputDevice;
@@ -75,7 +72,6 @@
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.policy.GestureNavigationSettingsObserver;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.FalsingManager;
@@ -94,7 +90,8 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.systemui.util.Assert;
+import com.android.systemui.util.concurrency.BackPanelUiThread;
+import com.android.systemui.util.concurrency.UiThreadContext;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.pip.Pip;
@@ -136,7 +133,7 @@
                 public void onSystemGestureExclusionChanged(int displayId,
                         Region systemGestureExclusion, Region unrestrictedOrNull) {
                     if (displayId == mDisplayId) {
-                        mMainExecutor.execute(() -> {
+                        mUiThreadContext.getExecutor().execute(() -> {
                             mExcludeRegion.set(systemGestureExclusion);
                             mUnrestrictedExcludeRegion.set(unrestrictedOrNull != null
                                     ? unrestrictedOrNull : systemGestureExclusion);
@@ -215,8 +212,7 @@
     private final Point mDisplaySize = new Point();
     private final int mDisplayId;
 
-    private final Executor mMainExecutor;
-    private final Handler mMainHandler;
+    private final UiThreadContext mUiThreadContext;
     private final Executor mBackgroundExecutor;
 
     private final Rect mPipExcludedBounds = new Rect();
@@ -411,8 +407,7 @@
             OverviewProxyService overviewProxyService,
             SysUiState sysUiState,
             PluginManager pluginManager,
-            @Main Executor executor,
-            @Main Handler handler,
+            @BackPanelUiThread UiThreadContext uiThreadContext,
             @Background Executor backgroundExecutor,
             UserTracker userTracker,
             NavigationModeController navigationModeController,
@@ -428,8 +423,7 @@
             Provider<LightBarController> lightBarControllerProvider) {
         mContext = context;
         mDisplayId = context.getDisplayId();
-        mMainExecutor = executor;
-        mMainHandler = handler;
+        mUiThreadContext = uiThreadContext;
         mBackgroundExecutor = backgroundExecutor;
         mUserTracker = userTracker;
         mOverviewProxyService = overviewProxyService;
@@ -478,7 +472,7 @@
                 ViewConfiguration.getLongPressTimeout());
 
         mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver(
-                mMainHandler, mContext, this::onNavigationSettingsChanged);
+                mUiThreadContext.getHandler(), mContext, this::onNavigationSettingsChanged);
 
         updateCurrentUserResources();
     }
@@ -506,11 +500,13 @@
         final boolean previousForcedVisible = mIsButtonForcedVisible;
         mIsButtonForcedVisible =
                 mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible();
+        // Update this before calling mButtonForcedVisibleCallback since NavigationBar will relayout
+        // and query isHandlingGestures() as a part of the callback
+        mIsBackGestureAllowed = !mIsButtonForcedVisible;
         if (previousForcedVisible != mIsButtonForcedVisible
                 && mButtonForcedVisibleCallback != null) {
             mButtonForcedVisibleCallback.accept(mIsButtonForcedVisible);
         }
-        mIsBackGestureAllowed = !mIsButtonForcedVisible;
 
         final DisplayMetrics dm = res.getDisplayMetrics();
         final float defaultGestureHeight = res.getDimension(
@@ -564,13 +560,15 @@
         mIsAttached = true;
         mOverviewProxyService.addCallback(mQuickSwitchListener);
         mSysUiState.addCallback(mSysUiStateCallback);
-        mInputManager.registerInputDeviceListener(mInputDeviceListener, mMainHandler);
-        int [] inputDevices = mInputManager.getInputDeviceIds();
+        mInputManager.registerInputDeviceListener(
+                mInputDeviceListener,
+                mUiThreadContext.getHandler());
+        int[] inputDevices = mInputManager.getInputDeviceIds();
         for (int inputDeviceId : inputDevices) {
             mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
         }
         updateIsEnabled();
-        mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+        mUserTracker.addCallback(mUserChangedCallback, mUiThreadContext.getExecutor());
     }
 
     /**
@@ -617,6 +615,10 @@
     }
 
     private void updateIsEnabled() {
+        mUiThreadContext.runWithScissors(this::updateIsEnabledInner);
+    }
+
+    private void updateIsEnabledInner() {
         try {
             Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
 
@@ -661,12 +663,12 @@
                 TaskStackChangeListeners.getInstance().registerTaskStackListener(
                         mTaskStackListener);
                 DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                        mMainExecutor::execute, mOnPropertiesChangedListener);
+                        mUiThreadContext.getExecutor()::execute, mOnPropertiesChangedListener);
                 mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(
                         mOnIsInPipStateChangedListener));
                 mDesktopModeOptional.ifPresent(
                         dm -> dm.addDesktopGestureExclusionRegionListener(
-                                mDesktopCornersChangedListener, mMainExecutor));
+                                mDesktopCornersChangedListener, mUiThreadContext.getExecutor()));
 
                 try {
                     mWindowManagerService.registerSystemGestureExclusionListener(
@@ -677,8 +679,8 @@
 
                 // Register input event receiver
                 mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId);
-                mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
-                        Choreographer.getInstance(), this::onInputEvent);
+                mInputEventReceiver = mInputMonitor.getInputReceiver(mUiThreadContext.getLooper(),
+                        mUiThreadContext.getChoreographer(), this::onInputEvent);
 
                 // Add a nav bar panel window
                 resetEdgeBackPlugin();
@@ -773,7 +775,7 @@
         mUseMLModel = newState;
 
         if (mUseMLModel) {
-            Assert.isMainThread();
+            mUiThreadContext.isCurrentThread();
             if (mMLModelIsLoading) {
                 Log.d(TAG, "Model tried to load while already loading.");
                 return;
@@ -804,12 +806,13 @@
         }
         BackGestureTfClassifierProvider finalProvider = provider;
         Map<String, Integer> finalVocab = vocab;
-        mMainExecutor.execute(() -> onMLModelLoadFinished(finalProvider, finalVocab, threshold));
+        mUiThreadContext.getExecutor().execute(
+                () -> onMLModelLoadFinished(finalProvider, finalVocab, threshold));
     }
 
     private void onMLModelLoadFinished(BackGestureTfClassifierProvider provider,
             Map<String, Integer> vocab, float threshold) {
-        Assert.isMainThread();
+        mUiThreadContext.isCurrentThread();
         mMLModelIsLoading = false;
         if (!mUseMLModel) {
             // This can happen if the user disables Gesture Nav while the model is loading.
@@ -1291,7 +1294,7 @@
         updateBackAnimationThresholds();
         if (mLightBarControllerProvider.get() != null) {
             mBackAnimation.setStatusBarCustomizer((appearance) -> {
-                mMainExecutor.execute(() ->
+                mUiThreadContext.getExecutor().execute(() ->
                         mLightBarControllerProvider.get()
                                 .customizeStatusBarAppearance(appearance));
             });
@@ -1308,8 +1311,7 @@
         private final OverviewProxyService mOverviewProxyService;
         private final SysUiState mSysUiState;
         private final PluginManager mPluginManager;
-        private final Executor mExecutor;
-        private final Handler mHandler;
+        private final UiThreadContext mUiThreadContext;
         private final Executor mBackgroundExecutor;
         private final UserTracker mUserTracker;
         private final NavigationModeController mNavigationModeController;
@@ -1327,29 +1329,27 @@
 
         @Inject
         public Factory(OverviewProxyService overviewProxyService,
-                       SysUiState sysUiState,
-                       PluginManager pluginManager,
-                       @Main Executor executor,
-                       @Main Handler handler,
-                       @Background Executor backgroundExecutor,
-                       UserTracker userTracker,
-                       NavigationModeController navigationModeController,
-                       BackPanelController.Factory backPanelControllerFactory,
-                       ViewConfiguration viewConfiguration,
-                       WindowManager windowManager,
-                       IWindowManager windowManagerService,
-                       InputManager inputManager,
-                       Optional<Pip> pipOptional,
-                       Optional<DesktopMode> desktopModeOptional,
-                       FalsingManager falsingManager,
-                       Provider<BackGestureTfClassifierProvider>
-                               backGestureTfClassifierProviderProvider,
-                       Provider<LightBarController> lightBarControllerProvider) {
+                        SysUiState sysUiState,
+                        PluginManager pluginManager,
+                        @BackPanelUiThread UiThreadContext uiThreadContext,
+                        @Background Executor backgroundExecutor,
+                        UserTracker userTracker,
+                        NavigationModeController navigationModeController,
+                        BackPanelController.Factory backPanelControllerFactory,
+                        ViewConfiguration viewConfiguration,
+                        WindowManager windowManager,
+                        IWindowManager windowManagerService,
+                        InputManager inputManager,
+                        Optional<Pip> pipOptional,
+                        Optional<DesktopMode> desktopModeOptional,
+                        FalsingManager falsingManager,
+                        Provider<BackGestureTfClassifierProvider>
+                                backGestureTfClassifierProviderProvider,
+                        Provider<LightBarController> lightBarControllerProvider) {
             mOverviewProxyService = overviewProxyService;
             mSysUiState = sysUiState;
             mPluginManager = pluginManager;
-            mExecutor = executor;
-            mHandler = handler;
+            mUiThreadContext = uiThreadContext;
             mBackgroundExecutor = backgroundExecutor;
             mUserTracker = userTracker;
             mNavigationModeController = navigationModeController;
@@ -1367,26 +1367,26 @@
 
         /** Construct a {@link EdgeBackGestureHandler}. */
         public EdgeBackGestureHandler create(Context context) {
-            return new EdgeBackGestureHandler(
-                    context,
-                    mOverviewProxyService,
-                    mSysUiState,
-                    mPluginManager,
-                    mExecutor,
-                    mHandler,
-                    mBackgroundExecutor,
-                    mUserTracker,
-                    mNavigationModeController,
-                    mBackPanelControllerFactory,
-                    mViewConfiguration,
-                    mWindowManager,
-                    mWindowManagerService,
-                    mInputManager,
-                    mPipOptional,
-                    mDesktopModeOptional,
-                    mFalsingManager,
-                    mBackGestureTfClassifierProviderProvider,
-                    mLightBarControllerProvider);
+            return mUiThreadContext.runWithScissors(
+                    () -> new EdgeBackGestureHandler(
+                            context,
+                            mOverviewProxyService,
+                            mSysUiState,
+                            mPluginManager,
+                            mUiThreadContext,
+                            mBackgroundExecutor,
+                            mUserTracker,
+                            mNavigationModeController,
+                            mBackPanelControllerFactory,
+                            mViewConfiguration,
+                            mWindowManager,
+                            mWindowManagerService,
+                            mInputManager,
+                            mPipOptional,
+                            mDesktopModeOptional,
+                            mFalsingManager,
+                            mBackGestureTfClassifierProviderProvider,
+                            mLightBarControllerProvider));
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index 439b7e1..db8749f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -10,92 +10,114 @@
 data class EdgePanelParams(private var resources: Resources) {
 
     data class ArrowDimens(
-            val length: Float? = 0f,
-            val height: Float? = 0f,
-            val alpha: Float = 0f,
-            val heightSpring: SpringForce? = null,
-            val lengthSpring: SpringForce? = null,
-            var alphaSpring: Step<SpringForce>? = null,
-            var alphaInterpolator: Step<Float>? = null
+        val length: Float? = 0f,
+        val height: Float? = 0f,
+        val alpha: Float = 0f,
+        val heightSpring: SpringForce? = null,
+        val lengthSpring: SpringForce? = null,
+        var alphaSpring: Step<SpringForce>? = null,
+        var alphaInterpolator: Step<Float>? = null
     )
 
     data class BackgroundDimens(
-            val width: Float? = 0f,
-            val height: Float = 0f,
-            val edgeCornerRadius: Float = 0f,
-            val farCornerRadius: Float = 0f,
-            val alpha: Float = 0f,
-            val widthSpring: SpringForce? = null,
-            val heightSpring: SpringForce? = null,
-            val farCornerRadiusSpring: SpringForce? = null,
-            val edgeCornerRadiusSpring: SpringForce? = null,
-            val alphaSpring: SpringForce? = null,
+        val width: Float? = 0f,
+        val height: Float = 0f,
+        val edgeCornerRadius: Float = 0f,
+        val farCornerRadius: Float = 0f,
+        val alpha: Float = 0f,
+        val widthSpring: SpringForce? = null,
+        val heightSpring: SpringForce? = null,
+        val farCornerRadiusSpring: SpringForce? = null,
+        val edgeCornerRadiusSpring: SpringForce? = null,
+        val alphaSpring: SpringForce? = null,
     )
 
     data class BackIndicatorDimens(
-            val horizontalTranslation: Float? = 0f,
-            val scale: Float = 0f,
-            val scalePivotX: Float? = null,
-            val arrowDimens: ArrowDimens,
-            val backgroundDimens: BackgroundDimens,
-            val verticalTranslationSpring: SpringForce? = null,
-            val horizontalTranslationSpring: SpringForce? = null,
-            val scaleSpring: SpringForce? = null,
+        val horizontalTranslation: Float? = 0f,
+        val scale: Float = 0f,
+        val scalePivotX: Float? = null,
+        val arrowDimens: ArrowDimens,
+        val backgroundDimens: BackgroundDimens,
+        val verticalTranslationSpring: SpringForce? = null,
+        val horizontalTranslationSpring: SpringForce? = null,
+        val scaleSpring: SpringForce? = null,
     )
 
     lateinit var entryIndicator: BackIndicatorDimens
         private set
+
     lateinit var activeIndicator: BackIndicatorDimens
         private set
+
     lateinit var cancelledIndicator: BackIndicatorDimens
         private set
+
     lateinit var flungIndicator: BackIndicatorDimens
         private set
+
     lateinit var committedIndicator: BackIndicatorDimens
         private set
+
     lateinit var preThresholdIndicator: BackIndicatorDimens
         private set
+
     lateinit var fullyStretchedIndicator: BackIndicatorDimens
         private set
 
     // navigation bar edge constants
     var arrowPaddingEnd: Int = 0
         private set
+
     var arrowThickness: Float = 0f
         private set
+
     // The closest to y
     var minArrowYPosition: Int = 0
         private set
+
     var fingerOffset: Int = 0
         private set
+
     var staticTriggerThreshold: Float = 0f
         private set
+
     var reactivationTriggerThreshold: Float = 0f
         private set
+
     var deactivationTriggerThreshold: Float = 0f
         get() = -field
         private set
+
     lateinit var dynamicTriggerThresholdRange: ClosedRange<Float>
         private set
+
     var swipeProgressThreshold: Float = 0f
         private set
 
     lateinit var entryWidthInterpolator: Interpolator
         private set
+
     lateinit var entryWidthTowardsEdgeInterpolator: Interpolator
         private set
+
     lateinit var activeWidthInterpolator: Interpolator
         private set
+
     lateinit var arrowAngleInterpolator: Interpolator
         private set
+
     lateinit var horizontalTranslationInterpolator: Interpolator
         private set
+
     lateinit var verticalTranslationInterpolator: Interpolator
         private set
+
     lateinit var farCornerInterpolator: Interpolator
         private set
+
     lateinit var edgeCornerInterpolator: Interpolator
         private set
+
     lateinit var heightInterpolator: Interpolator
         private set
 
@@ -108,7 +130,10 @@
     }
 
     private fun getDimenFloat(id: Int): Float {
-        return TypedValue().run { resources.getValue(id, this, true); float }
+        return TypedValue().run {
+            resources.getValue(id, this, true)
+            float
+        }
     }
 
     private fun getPx(id: Int): Int {
@@ -123,11 +148,10 @@
         fingerOffset = getPx(R.dimen.navigation_edge_finger_offset)
         staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
         reactivationTriggerThreshold =
-                getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
+            getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
         deactivationTriggerThreshold =
-                getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
-        dynamicTriggerThresholdRange =
-                reactivationTriggerThreshold..deactivationTriggerThreshold
+            getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
+        dynamicTriggerThresholdRange = reactivationTriggerThreshold..deactivationTriggerThreshold
         swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
 
         entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
@@ -149,27 +173,31 @@
 
         val commonArrowDimensAlphaThreshold = .165f
         val commonArrowDimensAlphaFactor = 1.05f
-        val commonArrowDimensAlphaSpring = Step(
-            threshold = commonArrowDimensAlphaThreshold,
-            factor = commonArrowDimensAlphaFactor,
-            postThreshold = createSpring(180f, 0.9f),
-            preThreshold = createSpring(2000f, 0.6f)
-        )
-        val commonArrowDimensAlphaSpringInterpolator = Step(
-            threshold = commonArrowDimensAlphaThreshold,
-            factor = commonArrowDimensAlphaFactor,
-            postThreshold = 1f,
-            preThreshold = 0f
-        )
+        val commonArrowDimensAlphaSpring =
+            Step(
+                threshold = commonArrowDimensAlphaThreshold,
+                factor = commonArrowDimensAlphaFactor,
+                postThreshold = createSpring(180f, 0.9f),
+                preThreshold = createSpring(2000f, 0.6f)
+            )
+        val commonArrowDimensAlphaSpringInterpolator =
+            Step(
+                threshold = commonArrowDimensAlphaThreshold,
+                factor = commonArrowDimensAlphaFactor,
+                postThreshold = 1f,
+                preThreshold = 0f
+            )
 
-        entryIndicator = BackIndicatorDimens(
+        entryIndicator =
+            BackIndicatorDimens(
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
                 scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
                 horizontalTranslationSpring = createSpring(800f, 0.76f),
                 verticalTranslationSpring = createSpring(30000f, 1f),
                 scaleSpring = createSpring(120f, 0.8f),
-                arrowDimens = ArrowDimens(
+                arrowDimens =
+                    ArrowDimens(
                         length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
                         height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
                         alpha = 0f,
@@ -177,8 +205,9 @@
                         heightSpring = createSpring(600f, 0.4f),
                         alphaSpring = commonArrowDimensAlphaSpring,
                         alphaInterpolator = commonArrowDimensAlphaSpringInterpolator
-                ),
-                backgroundDimens = BackgroundDimens(
+                    ),
+                backgroundDimens =
+                    BackgroundDimens(
                         alpha = 1f,
                         width = getDimen(R.dimen.navigation_edge_entry_background_width),
                         height = getDimen(R.dimen.navigation_edge_entry_background_height),
@@ -188,16 +217,18 @@
                         heightSpring = createSpring(1500f, 0.45f),
                         farCornerRadiusSpring = createSpring(300f, 0.5f),
                         edgeCornerRadiusSpring = createSpring(150f, 0.5f),
-                )
-        )
+                    )
+            )
 
-        activeIndicator = BackIndicatorDimens(
+        activeIndicator =
+            BackIndicatorDimens(
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
                 horizontalTranslationSpring = createSpring(1000f, 0.8f),
                 scaleSpring = createSpring(325f, 0.55f),
                 scalePivotX = getDimen(R.dimen.navigation_edge_active_background_width),
-                arrowDimens = ArrowDimens(
+                arrowDimens =
+                    ArrowDimens(
                         length = getDimen(R.dimen.navigation_edge_active_arrow_length),
                         height = getDimen(R.dimen.navigation_edge_active_arrow_height),
                         alpha = 1f,
@@ -205,8 +236,9 @@
                         heightSpring = activeCommittedArrowHeightSpring,
                         alphaSpring = commonArrowDimensAlphaSpring,
                         alphaInterpolator = commonArrowDimensAlphaSpringInterpolator
-                ),
-                backgroundDimens = BackgroundDimens(
+                    ),
+                backgroundDimens =
+                    BackgroundDimens(
                         alpha = 1f,
                         width = getDimen(R.dimen.navigation_edge_active_background_width),
                         height = getDimen(R.dimen.navigation_edge_active_background_height),
@@ -216,16 +248,18 @@
                         heightSpring = createSpring(10000f, 1f),
                         edgeCornerRadiusSpring = createSpring(2600f, 0.855f),
                         farCornerRadiusSpring = createSpring(1200f, 0.30f),
-                )
-        )
+                    )
+            )
 
-        preThresholdIndicator = BackIndicatorDimens(
+        preThresholdIndicator =
+            BackIndicatorDimens(
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale),
                 scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
                 scaleSpring = createSpring(120f, 0.8f),
                 horizontalTranslationSpring = createSpring(6000f, 1f),
-                arrowDimens = ArrowDimens(
+                arrowDimens =
+                    ArrowDimens(
                         length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length),
                         height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height),
                         alpha = 1f,
@@ -233,32 +267,36 @@
                         heightSpring = createSpring(100f, 0.6f),
                         alphaSpring = commonArrowDimensAlphaSpring,
                         alphaInterpolator = commonArrowDimensAlphaSpringInterpolator
-                ),
-                backgroundDimens = BackgroundDimens(
+                    ),
+                backgroundDimens =
+                    BackgroundDimens(
                         alpha = 1f,
                         width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
                         height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
                         edgeCornerRadius =
-                                getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
+                            getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
                         farCornerRadius =
-                                getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
+                            getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
                         widthSpring = createSpring(650f, 1f),
                         heightSpring = createSpring(1500f, 0.45f),
                         farCornerRadiusSpring = createSpring(300f, 1f),
                         edgeCornerRadiusSpring = createSpring(250f, 0.5f),
-                )
-        )
+                    )
+            )
 
-        committedIndicator = activeIndicator.copy(
+        committedIndicator =
+            activeIndicator.copy(
                 horizontalTranslation = null,
                 scalePivotX = null,
-                arrowDimens = activeIndicator.arrowDimens.copy(
+                arrowDimens =
+                    activeIndicator.arrowDimens.copy(
                         lengthSpring = activeCommittedArrowLengthSpring,
                         heightSpring = activeCommittedArrowHeightSpring,
                         length = null,
                         height = null,
-                ),
-                backgroundDimens = activeIndicator.backgroundDimens.copy(
+                    ),
+                backgroundDimens =
+                    activeIndicator.backgroundDimens.copy(
                         alpha = 0f,
                         // explicitly set to null to preserve previous width upon state change
                         width = null,
@@ -267,49 +305,57 @@
                         edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
                         farCornerRadiusSpring = flungCommittedFarCornerSpring,
                         alphaSpring = createSpring(1400f, 1f),
-                ),
+                    ),
                 scale = 0.86f,
                 scaleSpring = createSpring(5700f, 1f),
-        )
+            )
 
-        flungIndicator = committedIndicator.copy(
-                arrowDimens = committedIndicator.arrowDimens.copy(
+        flungIndicator =
+            committedIndicator.copy(
+                arrowDimens =
+                    committedIndicator.arrowDimens.copy(
                         lengthSpring = createSpring(850f, 0.46f),
                         heightSpring = createSpring(850f, 0.46f),
                         length = activeIndicator.arrowDimens.length,
                         height = activeIndicator.arrowDimens.height
-                ),
-                backgroundDimens = committedIndicator.backgroundDimens.copy(
+                    ),
+                backgroundDimens =
+                    committedIndicator.backgroundDimens.copy(
                         widthSpring = flungCommittedWidthSpring,
                         heightSpring = flungCommittedHeightSpring,
                         edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
                         farCornerRadiusSpring = flungCommittedFarCornerSpring,
-                )
-        )
+                    )
+            )
 
-        cancelledIndicator = entryIndicator.copy(
-                backgroundDimens = entryIndicator.backgroundDimens.copy(
+        cancelledIndicator =
+            entryIndicator.copy(
+                backgroundDimens =
+                    entryIndicator.backgroundDimens.copy(
                         width = 0f,
                         alpha = 0f,
                         alphaSpring = createSpring(450f, 1f)
-                )
-        )
+                    )
+            )
 
-        fullyStretchedIndicator = BackIndicatorDimens(
+        fullyStretchedIndicator =
+            BackIndicatorDimens(
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale),
                 horizontalTranslationSpring = null,
                 verticalTranslationSpring = null,
                 scaleSpring = null,
-                arrowDimens = ArrowDimens(
+                arrowDimens =
+                    ArrowDimens(
                         length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
                         height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
                         alpha = 1f,
                         alphaSpring = null,
                         heightSpring = null,
                         lengthSpring = null,
-                ),
-                backgroundDimens = BackgroundDimens(
+                    ),
+                backgroundDimens =
+                    BackgroundDimens(
                         alpha = 1f,
                         width = getDimen(R.dimen.navigation_edge_stretch_background_width),
                         height = getDimen(R.dimen.navigation_edge_stretch_background_height),
@@ -320,11 +366,11 @@
                         heightSpring = null,
                         edgeCornerRadiusSpring = null,
                         farCornerRadiusSpring = null,
-                )
-        )
+                    )
+            )
     }
 }
 
 fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce {
     return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio)
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt
index deb0fed..954e94a 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.kt
@@ -26,11 +26,7 @@
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.compose.theme.PlatformTheme
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.people.ui.compose.PeopleScreen
-import com.android.systemui.people.ui.view.PeopleViewBinder
-import com.android.systemui.people.ui.view.PeopleViewBinder.bind
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.launch
@@ -38,10 +34,7 @@
 /** People Tile Widget configuration activity that shows the user their conversation tiles. */
 class PeopleSpaceActivity
 @Inject
-constructor(
-    private val viewModelFactory: PeopleViewModel.Factory,
-    private val featureFlags: FeatureFlags,
-) : ComponentActivity() {
+constructor(private val viewModelFactory: PeopleViewModel.Factory) : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setResult(RESULT_CANCELED)
@@ -66,17 +59,7 @@
         }
 
         // Set the content of the activity, using either the View or Compose implementation.
-        if (featureFlags.isEnabled(Flags.COMPOSE_PEOPLE_SPACE)) {
-            Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity")
-            setContent {
-                PlatformTheme { PeopleScreen(viewModel, onResult = { finishActivity(it) }) }
-            }
-        } else {
-            Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity")
-            val view = PeopleViewBinder.create(this)
-            bind(view, viewModel, lifecycleOwner = this, onResult = { finishActivity(it) })
-            setContentView(view)
-        }
+        setContent { PlatformTheme { PeopleScreen(viewModel, onResult = { finishActivity(it) }) } }
     }
 
     private fun finishActivity(result: PeopleViewModel.Result) {
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java
deleted file mode 100644
index 59c76ad..0000000
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceTileView.java
+++ /dev/null
@@ -1,81 +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.people;
-
-import android.app.people.PeopleSpaceTile;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.graphics.Bitmap;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.systemui.res.R;
-
-/**
- * PeopleSpaceTileView renders an individual person's tile with associated status.
- */
-public class PeopleSpaceTileView extends LinearLayout {
-
-    private View mTileView;
-    private TextView mNameView;
-    private ImageView mPersonIconView;
-
-    public PeopleSpaceTileView(Context context, ViewGroup view, String shortcutId, boolean isLast) {
-        super(context);
-        mTileView = view.findViewWithTag(shortcutId);
-        if (mTileView == null) {
-            LayoutInflater inflater = LayoutInflater.from(context);
-            mTileView = inflater.inflate(R.layout.people_space_tile_view, view, false);
-            view.addView(mTileView, LayoutParams.MATCH_PARENT,
-                    LayoutParams.MATCH_PARENT);
-            mTileView.setTag(shortcutId);
-
-            // If it's not the last conversation in this section, add a divider.
-            if (!isLast) {
-                inflater.inflate(R.layout.people_space_activity_list_divider, view, true);
-            }
-        }
-        mNameView = mTileView.findViewById(R.id.tile_view_name);
-        mPersonIconView = mTileView.findViewById(R.id.tile_view_person_icon);
-    }
-
-    /** Sets the name text on the tile. */
-    public void setName(String name) {
-        mNameView.setText(name);
-    }
-
-    /** Sets the person and package drawable on the tile. */
-    public void setPersonIcon(Bitmap bitmap) {
-        mPersonIconView.setImageBitmap(bitmap);
-    }
-
-    /** Sets the click listener of the tile. */
-    public void setOnClickListener(LauncherApps launcherApps, PeopleSpaceTile tile) {
-        mTileView.setOnClickListener(v ->
-                launcherApps.startShortcut(tile.getPackageName(), tile.getId(), null, null,
-                        tile.getUserHandle()));
-    }
-
-    /** Sets the click listener of the tile directly. */
-    public void setOnClickListener(OnClickListener onClickListener) {
-        mTileView.setOnClickListener(onClickListener);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt b/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
deleted file mode 100644
index 10a2b3c..0000000
--- a/packages/SystemUI/src/com/android/systemui/people/ui/view/PeopleViewBinder.kt
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.people.ui.view
-
-import android.content.Context
-import android.graphics.Color
-import android.graphics.Outline
-import android.graphics.drawable.GradientDrawable
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewOutlineProvider
-import android.widget.LinearLayout
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.Lifecycle.State.CREATED
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.people.PeopleSpaceTileView
-import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
-import com.android.systemui.people.ui.viewmodel.PeopleViewModel
-import com.android.systemui.res.R
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-/** A ViewBinder for [PeopleViewModel]. */
-object PeopleViewBinder {
-    private const val TAG = "PeopleViewBinder"
-
-    /**
-     * The [ViewOutlineProvider] used to clip the corner radius of the recent and priority lists.
-     */
-    private val ViewOutlineProvider =
-        object : ViewOutlineProvider() {
-            override fun getOutline(view: View, outline: Outline) {
-                outline.setRoundRect(
-                    0,
-                    0,
-                    view.width,
-                    view.height,
-                    view.context.resources.getDimension(R.dimen.people_space_widget_radius),
-                )
-            }
-        }
-
-    /** Create a [View] that can later be [bound][bind] to a [PeopleViewModel]. */
-    @JvmStatic
-    fun create(context: Context): ViewGroup {
-        return LayoutInflater.from(context)
-            .inflate(R.layout.people_space_activity, /* root= */ null) as ViewGroup
-    }
-
-    /** Bind [view] to [viewModel]. */
-    @JvmStatic
-    fun bind(
-        view: ViewGroup,
-        viewModel: PeopleViewModel,
-        lifecycleOwner: LifecycleOwner,
-        onResult: (PeopleViewModel.Result) -> Unit,
-    ) {
-        // Call [onResult] as soon as a result is available.
-        lifecycleOwner.lifecycleScope.launch {
-            lifecycleOwner.repeatOnLifecycle(CREATED) {
-                viewModel.result.collect { result ->
-                    if (result != null) {
-                        viewModel.clearResult()
-                        onResult(result)
-                    }
-                }
-            }
-        }
-
-        // Start collecting the UI data once the Activity is STARTED.
-        lifecycleOwner.lifecycleScope.launch {
-            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                combine(
-                        viewModel.priorityTiles,
-                        viewModel.recentTiles,
-                    ) { priority, recent ->
-                        priority to recent
-                    }
-                    .collect { (priorityTiles, recentTiles) ->
-                        if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
-                            setConversationsContent(
-                                view,
-                                priorityTiles,
-                                recentTiles,
-                                viewModel.onTileClicked,
-                            )
-                        } else {
-                            setNoConversationsContent(view, viewModel.onUserJourneyCancelled)
-                        }
-                    }
-            }
-        }
-    }
-
-    private fun setNoConversationsContent(view: ViewGroup, onGotItClicked: () -> Unit) {
-        // This should never happen.
-        if (view.childCount > 1) {
-            error("view has ${view.childCount} children, it should have maximum 1")
-        }
-
-        // The static content for no conversations is already shown.
-        if (view.findViewById<View>(R.id.top_level_no_conversations) != null) {
-            return
-        }
-
-        // If we were showing the content with conversations earlier, remove it.
-        if (view.childCount == 1) {
-            view.removeViewAt(0)
-        }
-
-        val context = view.context
-        val noConversationsView =
-            LayoutInflater.from(context)
-                .inflate(R.layout.people_space_activity_no_conversations, /* root= */ view)
-
-        noConversationsView.requireViewById<View>(R.id.got_it_button).setOnClickListener {
-            onGotItClicked()
-        }
-
-        // The Tile preview has colorBackground as its background. Change it so it's different than
-        // the activity's background.
-        val item = noConversationsView.requireViewById<LinearLayout>(android.R.id.background)
-        val shape = item.background as GradientDrawable
-        val ta =
-            context.theme.obtainStyledAttributes(
-                intArrayOf(com.android.internal.R.attr.colorSurface)
-            )
-        shape.setColor(ta.getColor(0, Color.WHITE))
-        ta.recycle()
-    }
-
-    private fun setConversationsContent(
-        view: ViewGroup,
-        priorityTiles: List<PeopleTileViewModel>,
-        recentTiles: List<PeopleTileViewModel>,
-        onTileClicked: (PeopleTileViewModel) -> Unit,
-    ) {
-        // This should never happen.
-        if (view.childCount > 1) {
-            error("view has ${view.childCount} children, it should have maximum 1")
-        }
-
-        // Inflate the content with conversations, if it's not already.
-        if (view.findViewById<View>(R.id.top_level_with_conversations) == null) {
-            // If we were showing the content without conversations earlier, remove it.
-            if (view.childCount == 1) {
-                view.removeViewAt(0)
-            }
-
-            LayoutInflater.from(view.context)
-                .inflate(R.layout.people_space_activity_with_conversations, /* root= */ view)
-        }
-
-        // TODO(b/193782241): Replace the NestedScrollView + 2x LinearLayout from this layout into a
-        // single RecyclerView once this screen is tested by screenshot tests. Introduce a
-        // PeopleSpaceTileViewBinder that will properly create and bind the View associated to a
-        // PeopleSpaceTileViewModel (and remove the PeopleSpaceTileView class).
-        val conversationsView = view.requireViewById<View>(R.id.top_level_with_conversations)
-        setTileViews(
-            conversationsView,
-            R.id.priority,
-            R.id.priority_tiles,
-            priorityTiles,
-            onTileClicked,
-        )
-
-        setTileViews(
-            conversationsView,
-            R.id.recent,
-            R.id.recent_tiles,
-            recentTiles,
-            onTileClicked,
-        )
-    }
-
-    /** Sets a [PeopleSpaceTileView]s for each conversation. */
-    private fun setTileViews(
-        root: View,
-        tilesListId: Int,
-        tilesId: Int,
-        tiles: List<PeopleTileViewModel>,
-        onTileClicked: (PeopleTileViewModel) -> Unit,
-    ) {
-        // Remove any previously added tile.
-        // TODO(b/193782241): Once this list is a big RecyclerView, set the current list and use
-        // DiffUtil to do as less addView/removeView as possible.
-        val layout = root.requireViewById<ViewGroup>(tilesId)
-        layout.removeAllViews()
-        layout.outlineProvider = ViewOutlineProvider
-
-        val tilesListView = root.requireViewById<LinearLayout>(tilesListId)
-        if (tiles.isEmpty()) {
-            tilesListView.visibility = View.GONE
-            return
-        }
-        tilesListView.visibility = View.VISIBLE
-
-        // Add each tile.
-        tiles.forEachIndexed { i, tile ->
-            val tileView =
-                PeopleSpaceTileView(root.context, layout, tile.key.shortcutId, i == tiles.size - 1)
-            bindTileView(tileView, tile, onTileClicked)
-        }
-    }
-
-    /** Sets [tileView] with the data in [conversation]. */
-    private fun bindTileView(
-        tileView: PeopleSpaceTileView,
-        tile: PeopleTileViewModel,
-        onTileClicked: (PeopleTileViewModel) -> Unit,
-    ) {
-        try {
-            tileView.setName(tile.username)
-            tileView.setPersonIcon(tile.icon)
-            tileView.setOnClickListener { onTileClicked(tile) }
-        } catch (e: Exception) {
-            Log.e(TAG, "Couldn't retrieve shortcut information", e)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 4ee2db7..cc0901f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -307,7 +307,7 @@
             } else {
                 // Set the horizontal paddings unless the view is the Compose implementation of the
                 // footer actions.
-                if (view.getTag(R.id.tag_compose_qs_footer_actions) == null) {
+                if (view.getId() != R.id.qs_footer_actions) {
                     view.setPaddingRelative(
                             mContentHorizontalPadding,
                             view.getPaddingTop(),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
index e424975..38d7290 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragmentLegacy.java
@@ -219,6 +219,13 @@
     }
 
     @Override
+    public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {
+        if (mQsImpl != null) {
+            mQsImpl.setShouldUpdateSquishinessOnMedia(shouldUpdate);
+        }
+    }
+
+    @Override
     public void setListening(boolean listening) {
         if (mQsImpl != null) {
             mQsImpl.setListening(listening);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 1f4838e..8c0d122 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -34,11 +34,11 @@
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.LinearLayout;
 
 import androidx.annotation.FloatRange;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.compose.ui.platform.ComposeView;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.LifecycleRegistry;
@@ -48,15 +48,12 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.controls.ui.view.MediaHost;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.qs.QSContainerController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSComponent;
-import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
@@ -117,11 +114,9 @@
     private final MediaHost mQqsMediaHost;
     private final QSDisableFlagsLogger mQsDisableFlagsLogger;
     private final LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
-    private final FeatureFlags mFeatureFlags;
     private final QSLogger mLogger;
     private final FooterActionsController mFooterActionsController;
     private final FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
-    private final FooterActionsViewBinder mFooterActionsViewBinder;
     private final ListeningAndVisibilityLifecycleOwner mListeningAndVisibilityLifecycleOwner;
     private boolean mShowCollapsedOnKeyguard;
     private boolean mLastKeyguardAndExpanded;
@@ -168,11 +163,14 @@
 
     private boolean mIsSmallScreen;
 
+    /** Should the squishiness fraction be updated on the media host. */
+    private boolean mShouldUpdateMediaSquishiness;
+
     private CommandQueue mCommandQueue;
 
     private View mRootView;
     @Nullable
-    private View mFooterActionsView;
+    private ComposeView mFooterActionsView;
 
     @Inject
     public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
@@ -184,23 +182,19 @@
             DumpManager dumpManager, QSLogger qsLogger,
             FooterActionsController footerActionsController,
             FooterActionsViewModel.Factory footerActionsViewModelFactory,
-            FooterActionsViewBinder footerActionsViewBinder,
-            LargeScreenShadeInterpolator largeScreenShadeInterpolator,
-            FeatureFlags featureFlags) {
+            LargeScreenShadeInterpolator largeScreenShadeInterpolator) {
         mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
         mQsMediaHost = qsMediaHost;
         mQqsMediaHost = qqsMediaHost;
         mQsDisableFlagsLogger = qsDisableFlagsLogger;
         mLogger = qsLogger;
         mLargeScreenShadeInterpolator = largeScreenShadeInterpolator;
-        mFeatureFlags = featureFlags;
         mCommandQueue = commandQueue;
         mBypassController = keyguardBypassController;
         mStatusBarStateController = statusBarStateController;
         mDumpManager = dumpManager;
         mFooterActionsController = footerActionsController;
         mFooterActionsViewModelFactory = footerActionsViewModelFactory;
-        mFooterActionsViewBinder = footerActionsViewBinder;
         mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
         if (SceneContainerFlag.isEnabled()) {
             mStatusBarState = StatusBarState.SHADE;
@@ -294,43 +288,9 @@
     }
 
     private void bindFooterActionsView(View root) {
-        LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions);
-
-        if (!mFeatureFlags.isEnabled(Flags.COMPOSE_QS_FOOTER_ACTIONS)) {
-            Log.d(TAG, "Binding the View implementation of the QS footer actions");
-            mFooterActionsView = footerActionsView;
-            mFooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
-                    mListeningAndVisibilityLifecycleOwner);
-            return;
-        }
-
-        // Compose is available, so let's use the Compose implementation of the footer actions.
-        Log.d(TAG, "Binding the Compose implementation of the QS footer actions");
-        View composeView = QSUtils.createFooterActionsView(root.getContext(),
+        mFooterActionsView = root.findViewById(R.id.qs_footer_actions);
+        QSUtils.setFooterActionsViewContent(mFooterActionsView,
                 mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
-        mFooterActionsView = composeView;
-
-        // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin
-        // to all views except for qs_footer_actions, so we set it to the Compose view.
-        composeView.setId(R.id.qs_footer_actions);
-
-        // Set this tag so that QSContainerImpl does not add horizontal paddings to this Compose
-        // implementation of the footer actions. They will be set in Compose instead so that the
-        // background fills the full screen width.
-        composeView.setTag(R.id.tag_compose_qs_footer_actions, true);
-
-        // Set the same elevation as the View implementation, otherwise the footer actions will be
-        // drawn below the scroll view with QS grid and clicks won't get through on small devices
-        // where there isn't enough vertical space to show all the tiles and the footer actions.
-        composeView.setElevation(
-                composeView.getContext().getResources().getDimension(R.dimen.qs_panel_elevation));
-
-        // Replace the View by the Compose provided one.
-        ViewGroup parent = (ViewGroup) footerActionsView.getParent();
-        ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams();
-        int index = parent.indexOfChild(footerActionsView);
-        parent.removeViewAt(index);
-        parent.addView(composeView, index, layoutParams);
     }
 
     @Override
@@ -662,6 +622,12 @@
     }
 
     @Override
+    public void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {
+        if (DEBUG) Log.d(TAG, "setShouldUpdateSquishinessOnMedia " + shouldUpdate);
+        mShouldUpdateMediaSquishiness = shouldUpdate;
+    }
+
+    @Override
     public void setQsExpansion(float expansion, float panelExpansionFraction,
             float proposedTranslation, float squishinessFraction) {
         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
@@ -740,9 +706,11 @@
         if (mQSAnimator != null) {
             mQSAnimator.setPosition(expansion);
         }
-        if (!mInSplitShade
+        if (!mShouldUpdateMediaSquishiness
+                && (!mInSplitShade
                 || mStatusBarStateController.getState() == KEYGUARD
-                || mStatusBarStateController.getState() == SHADE_LOCKED) {
+                || mStatusBarStateController.getState() == SHADE_LOCKED)
+        ) {
             // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen
             // and media player expect no change by squishiness in lock screen shade. Don't bother
             // squishing mQsMediaHost when not in split shade to prevent problems with stale state.
@@ -1038,6 +1006,7 @@
         indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
         indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
         indentingPw.println("mOverScrolling: " + mOverScrolling);
+        indentingPw.println("mShouldUpdateMediaSquishiness: " + mShouldUpdateMediaSquishiness);
         indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
         View view = getView();
         if (view != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt b/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt
index 15c3f27..5482e6d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSUtils.kt
@@ -1,10 +1,9 @@
 package com.android.systemui.qs
 
 import android.content.Context
-import android.view.View
+import androidx.compose.ui.platform.ComposeView
 import androidx.lifecycle.LifecycleOwner
 import com.android.compose.theme.PlatformTheme
-import com.android.compose.ui.platform.DensityAwareComposeView
 import com.android.internal.policy.SystemBarUtils
 import com.android.systemui.qs.footer.ui.compose.FooterActions
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
@@ -29,13 +28,11 @@
     }
 
     @JvmStatic
-    fun createFooterActionsView(
-        context: Context,
+    fun setFooterActionsViewContent(
+        view: ComposeView,
         viewModel: FooterActionsViewModel,
         qsVisibilityLifecycleOwner: LifecycleOwner,
-    ): View {
-        return DensityAwareComposeView(context).apply {
-            setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
-        }
+    ) {
+        view.setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
deleted file mode 100644
index 0995dd4..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/binder/FooterActionsViewBinder.kt
+++ /dev/null
@@ -1,329 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.footer.ui.binder
-
-import android.content.Context
-import android.graphics.PorterDuff
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.core.view.isVisible
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.animation.Expandable
-import com.android.systemui.common.ui.binder.IconViewBinder
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.people.ui.view.PeopleViewBinder.bind
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
-import com.android.systemui.res.R
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.launch
-
-/** A ViewBinder for [FooterActionsViewBinder]. */
-@SysUISingleton
-class FooterActionsViewBinder @Inject constructor() {
-    /** Create a view that can later be [bound][bind] to a [FooterActionsViewModel]. */
-    fun create(context: Context): LinearLayout {
-        return LayoutInflater.from(context).inflate(R.layout.footer_actions, /* root= */ null)
-            as LinearLayout
-    }
-
-    /** Bind [view] to [viewModel]. */
-    fun bind(
-        view: LinearLayout,
-        viewModel: FooterActionsViewModel,
-        qsVisibilityLifecycleOwner: LifecycleOwner,
-    ) {
-        view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
-
-        // Add the views used by this new implementation.
-        val context = view.context
-        val inflater = LayoutInflater.from(context)
-
-        val securityHolder = TextButtonViewHolder.createAndAdd(inflater, view)
-        val foregroundServicesWithTextHolder = TextButtonViewHolder.createAndAdd(inflater, view)
-        val foregroundServicesWithNumberHolder = NumberButtonViewHolder.createAndAdd(inflater, view)
-        val userSwitcherHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = false)
-        val settingsHolder =
-            IconButtonViewHolder.createAndAdd(inflater, view, isLast = viewModel.power == null)
-
-        // Bind the static power and settings buttons.
-        bindButton(settingsHolder, viewModel.settings)
-
-        if (viewModel.power != null) {
-            val powerHolder = IconButtonViewHolder.createAndAdd(inflater, view, isLast = true)
-            bindButton(powerHolder, viewModel.power)
-        }
-
-        // There are 2 lifecycle scopes we are using here:
-        //   1) The scope created by [repeatWhenAttached] when [view] is attached, and destroyed
-        //      when the [view] is detached. We use this as the parent scope for all our [viewModel]
-        //      state collection, given that we don't want to do any work when [view] is detached.
-        //   2) The scope owned by [lifecycleOwner], which should be RESUMED only when Quick
-        //      Settings are visible. We use this to make sure we collect UI state only when the
-        //      View is visible.
-        //
-        // Given that we start our collection when the Quick Settings become visible, which happens
-        // every time the user swipes down the shade, we remember our previous UI state already
-        // bound to the UI to avoid binding the same values over and over for nothing.
-
-        // TODO(b/242040009): Look into using only a single scope.
-
-        var previousSecurity: FooterActionsSecurityButtonViewModel? = null
-        var previousForegroundServices: FooterActionsForegroundServicesButtonViewModel? = null
-        var previousUserSwitcher: FooterActionsButtonViewModel? = null
-
-        // Listen for ViewModel updates when the View is attached.
-        view.repeatWhenAttached {
-            val attachedScope = this.lifecycleScope
-
-            attachedScope.launch {
-                // Listen for dialog requests as soon as we are attached, even when not visible.
-                // TODO(b/242040009): Should this move somewhere else?
-                launch { viewModel.observeDeviceMonitoringDialogRequests(view.context) }
-
-                // Make sure we set the correct alphas even when QS are not currently shown.
-                launch { viewModel.alpha.collect { view.alpha = it } }
-                launch {
-                    viewModel.backgroundAlpha.collect {
-                        view.background?.alpha = (it * 255).roundToInt()
-                    }
-                }
-            }
-
-            // Listen for model changes only when QS are visible.
-            qsVisibilityLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                // Security.
-                launch {
-                    viewModel.security.collect { security ->
-                        if (previousSecurity != security) {
-                            bindSecurity(view.context, securityHolder, security)
-                            previousSecurity = security
-                        }
-                    }
-                }
-
-                // Foreground services.
-                launch {
-                    viewModel.foregroundServices.collect { foregroundServices ->
-                        if (previousForegroundServices != foregroundServices) {
-                            bindForegroundService(
-                                foregroundServicesWithNumberHolder,
-                                foregroundServicesWithTextHolder,
-                                foregroundServices,
-                            )
-                            previousForegroundServices = foregroundServices
-                        }
-                    }
-                }
-
-                // User switcher.
-                launch {
-                    viewModel.userSwitcher.collect { userSwitcher ->
-                        if (previousUserSwitcher != userSwitcher) {
-                            bindButton(userSwitcherHolder, userSwitcher)
-                            previousUserSwitcher = userSwitcher
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    private fun bindSecurity(
-        quickSettingsContext: Context,
-        securityHolder: TextButtonViewHolder,
-        security: FooterActionsSecurityButtonViewModel?,
-    ) {
-        val securityView = securityHolder.view
-        securityView.isVisible = security != null
-        if (security == null) {
-            return
-        }
-
-        // Make sure that the chevron is visible and that the button is clickable if there is a
-        // listener.
-        val chevron = securityHolder.chevron
-        val onClick = security.onClick
-        if (onClick != null) {
-            securityView.isClickable = true
-            securityView.setOnClickListener {
-                onClick(quickSettingsContext, Expandable.fromView(securityView))
-            }
-            chevron.isVisible = true
-        } else {
-            securityView.isClickable = false
-            securityView.setOnClickListener(null)
-            chevron.isVisible = false
-        }
-
-        securityHolder.text.text = security.text
-        securityHolder.newDot.isVisible = false
-        IconViewBinder.bind(security.icon, securityHolder.icon)
-    }
-
-    private fun bindForegroundService(
-        foregroundServicesWithNumberHolder: NumberButtonViewHolder,
-        foregroundServicesWithTextHolder: TextButtonViewHolder,
-        foregroundServices: FooterActionsForegroundServicesButtonViewModel?,
-    ) {
-        val foregroundServicesWithNumberView = foregroundServicesWithNumberHolder.view
-        val foregroundServicesWithTextView = foregroundServicesWithTextHolder.view
-        if (foregroundServices == null) {
-            foregroundServicesWithNumberView.isVisible = false
-            foregroundServicesWithTextView.isVisible = false
-            return
-        }
-
-        val foregroundServicesCount = foregroundServices.foregroundServicesCount
-        if (foregroundServices.displayText) {
-            // Button with text, icon and chevron.
-            foregroundServicesWithNumberView.isVisible = false
-
-            foregroundServicesWithTextView.isVisible = true
-            foregroundServicesWithTextView.setOnClickListener {
-                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithTextView))
-            }
-            foregroundServicesWithTextHolder.text.text = foregroundServices.text
-            foregroundServicesWithTextHolder.newDot.isVisible = foregroundServices.hasNewChanges
-        } else {
-            // Small button with the number only.
-            foregroundServicesWithTextView.isVisible = false
-
-            foregroundServicesWithNumberView.isVisible = true
-            foregroundServicesWithNumberView.setOnClickListener {
-                foregroundServices.onClick(Expandable.fromView(foregroundServicesWithNumberView))
-            }
-            foregroundServicesWithNumberHolder.number.text = foregroundServicesCount.toString()
-            foregroundServicesWithNumberHolder.number.contentDescription = foregroundServices.text
-            foregroundServicesWithNumberHolder.newDot.isVisible = foregroundServices.hasNewChanges
-        }
-    }
-
-    private fun bindButton(button: IconButtonViewHolder, model: FooterActionsButtonViewModel?) {
-        val buttonView = button.view
-        buttonView.id = model?.id ?: View.NO_ID
-        buttonView.isVisible = model != null
-        if (model == null) {
-            return
-        }
-
-        val backgroundResource =
-            when (model.backgroundColor) {
-                R.attr.shadeInactive -> R.drawable.qs_footer_action_circle
-                R.attr.shadeActive -> R.drawable.qs_footer_action_circle_color
-                else -> error("Unsupported icon background resource ${model.backgroundColor}")
-            }
-        buttonView.setBackgroundResource(backgroundResource)
-        buttonView.setOnClickListener { model.onClick(Expandable.fromView(buttonView)) }
-
-        val icon = model.icon
-        val iconView = button.icon
-
-        IconViewBinder.bind(icon, iconView)
-        if (model.iconTint != null) {
-            iconView.setColorFilter(model.iconTint, PorterDuff.Mode.SRC_IN)
-        } else {
-            iconView.clearColorFilter()
-        }
-    }
-}
-
-private class TextButtonViewHolder(val view: View) {
-    val icon = view.requireViewById<ImageView>(R.id.icon)
-    val text = view.requireViewById<TextView>(R.id.text)
-    val newDot = view.requireViewById<ImageView>(R.id.new_dot)
-    val chevron = view.requireViewById<ImageView>(R.id.chevron_icon)
-
-    companion object {
-        fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): TextButtonViewHolder {
-            val view =
-                inflater.inflate(
-                    R.layout.footer_actions_text_button,
-                    /* root= */ root,
-                    /* attachToRoot= */ false,
-                )
-            root.addView(view)
-            return TextButtonViewHolder(view)
-        }
-    }
-}
-
-private class NumberButtonViewHolder(val view: View) {
-    val number = view.requireViewById<TextView>(R.id.number)
-    val newDot = view.requireViewById<ImageView>(R.id.new_dot)
-
-    companion object {
-        fun createAndAdd(inflater: LayoutInflater, root: ViewGroup): NumberButtonViewHolder {
-            val view =
-                inflater.inflate(
-                    R.layout.footer_actions_number_button,
-                    /* root= */ root,
-                    /* attachToRoot= */ false,
-                )
-            root.addView(view)
-            return NumberButtonViewHolder(view)
-        }
-    }
-}
-
-private class IconButtonViewHolder(val view: View) {
-    val icon = view.requireViewById<ImageView>(R.id.icon)
-
-    companion object {
-        fun createAndAdd(
-            inflater: LayoutInflater,
-            root: ViewGroup,
-            isLast: Boolean,
-        ): IconButtonViewHolder {
-            val view =
-                inflater.inflate(
-                    R.layout.footer_actions_icon_button,
-                    /* root= */ root,
-                    /* attachToRoot= */ false,
-                )
-
-            // All buttons have a background with an inset of qs_footer_action_inset, so the last
-            // button must have a negative inset of -qs_footer_action_inset to compensate and be
-            // aligned with its parent.
-            val marginEnd =
-                if (isLast) {
-                    -view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
-                } else {
-                    0
-                }
-
-            val size =
-                view.context.resources.getDimensionPixelSize(R.dimen.qs_footer_action_button_size)
-            root.addView(
-                view,
-                LinearLayout.LayoutParams(size, size).apply { this.marginEnd = marginEnd },
-            )
-            return IconButtonViewHolder(view)
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt
index e581bfc..095bdf2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt
@@ -19,38 +19,26 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 
-/** Repository for retrieving the list of [TileSpec] to be displayed as icons. */
+/** Repository for checking if a tile should be displayed as an icon. */
 interface IconTilesRepository {
-    val iconTilesSpecs: StateFlow<Set<TileSpec>>
+    fun isIconTile(spec: TileSpec): Boolean
 }
 
 @SysUISingleton
 class IconTilesRepositoryImpl @Inject constructor() : IconTilesRepository {
 
-    private val _iconTilesSpecs =
-        MutableStateFlow(
-            setOf(
-                TileSpec.create("airplane"),
-                TileSpec.create("battery"),
-                TileSpec.create("cameratoggle"),
-                TileSpec.create("cast"),
-                TileSpec.create("color_correction"),
-                TileSpec.create("inversion"),
-                TileSpec.create("saver"),
-                TileSpec.create("dnd"),
-                TileSpec.create("flashlight"),
-                TileSpec.create("location"),
-                TileSpec.create("mictoggle"),
-                TileSpec.create("nfc"),
-                TileSpec.create("night"),
-                TileSpec.create("rotation")
-            )
-        )
+    override fun isIconTile(spec: TileSpec): Boolean {
+        return !LARGE_TILES.contains(spec)
+    }
 
-    /** Set of toggleable tiles that are suitable for being shown as an icon. */
-    override val iconTilesSpecs: StateFlow<Set<TileSpec>> = _iconTilesSpecs.asStateFlow()
+    companion object {
+        private val LARGE_TILES =
+            setOf(
+                TileSpec.create("internet"),
+                TileSpec.create("bt"),
+                TileSpec.create("dnd"),
+                TileSpec.create("cast"),
+            )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
index ccc1c6e..524ea8b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -20,10 +20,9 @@
 import com.android.systemui.qs.panels.data.repository.IconTilesRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import javax.inject.Inject
-import kotlinx.coroutines.flow.StateFlow
 
 /** Interactor for retrieving the list of [TileSpec] to be displayed as icons. */
 @SysUISingleton
-class IconTilesInteractor @Inject constructor(repo: IconTilesRepository) {
-    val iconTilesSpecs: StateFlow<Set<TileSpec>> = repo.iconTilesSpecs
+class IconTilesInteractor @Inject constructor(private val repo: IconTilesRepository) {
+    fun isIconTile(spec: TileSpec): Boolean = repo.isIconTile(spec)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
index b437f64..e99c64c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
@@ -38,14 +38,13 @@
     override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> {
         val newTiles: MutableList<TileSpec> = mutableListOf()
         val row = TileRow<TileSpec>(columns = gridSizeInteractor.columns.value)
-        val iconTilesSet = iconTilesInteractor.iconTilesSpecs.value
         val tilesQueue =
             ArrayDeque(
                 tiles.map {
                     SizedTile(
                         it,
                         width =
-                            if (iconTilesSet.contains(it)) {
+                            if (iconTilesInteractor.isIconTile(it)) {
                                 1
                             } else {
                                 2
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 4aeaa7d..2f0fe22 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
@@ -52,15 +52,13 @@
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
             items(
                 tiles.size,
                 span = { index ->
-                    val iconOnly = iconTilesSpecs.contains(tiles[index].spec)
-                    if (iconOnly) {
+                    if (iconTilesViewModel.isIconTile(tiles[index].spec)) {
                         GridItemSpan(1)
                     } else {
                         GridItemSpan(2)
@@ -69,7 +67,7 @@
             ) { index ->
                 Tile(
                     tile = tiles[index],
-                    iconOnly = iconTilesSpecs.contains(tiles[index].spec),
+                    iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec),
                     modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
                 )
             }
@@ -83,12 +81,11 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit,
     ) {
-        val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         DefaultEditTileGrid(
             tiles = tiles,
-            iconOnlySpecs = iconOnlySpecs,
+            isIconOnly = iconTilesViewModel::isIconTile,
             columns = GridCells.Fixed(columns),
             modifier = modifier,
             onAddTile = onAddTile,
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 708ef0d..d600767 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
@@ -38,7 +38,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.dimensionResource
@@ -66,12 +65,11 @@
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        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) }
+        val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) }
 
         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
             // Large tiles
@@ -103,7 +101,6 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit
     ) {
-        val iconOnlySpecs by viewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val columns by viewModel.columns.collectAsStateWithLifecycle()
         val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
 
@@ -111,8 +108,6 @@
         val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
             onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
         }
-        val isIconOnly: (TileSpec) -> Boolean =
-            remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
         val largeTileHeight = tileHeight()
         val iconTileHeight = tileHeight(showLabels)
         val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
@@ -151,7 +146,7 @@
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 onRemoveTile = onRemoveTile,
-                isIconOnly = isIconOnly,
+                isIconOnly = viewModel::isIconTile,
                 columns = columns,
                 showLabels = showLabels,
             )
@@ -161,7 +156,7 @@
                 iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 addTileToEnd = addTileToEnd,
-                isIconOnly = isIconOnly,
+                isIconOnly = viewModel::isIconTile,
                 showLabels = showLabels,
                 columns = columns,
             )
@@ -232,7 +227,7 @@
         val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding)
         val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding)
         val largeGridHeightCustom =
-            gridHeight(tilesCustom.size, largeTileHeight, columns / 2, tilePadding)
+            gridHeight(tilesCustom.size, iconTileHeight, columns, tilePadding)
 
         // Add up the height of all three grids and add padding in between
         val gridHeight =
@@ -257,8 +252,14 @@
                 )
                 fillUpRow(nTiles = smallTiles.size, columns = columns)
 
-                // Custom tiles, all large
-                editTiles(tilesCustom, ClickAction.ADD, addTileToEnd, isIconOnly)
+                // Custom tiles, all icons
+                editTiles(
+                    tilesCustom,
+                    ClickAction.ADD,
+                    addTileToEnd,
+                    isIconOnly,
+                    showLabels = showLabels
+                )
             }
         }
     }
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 70d629f..7f4e0a7 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
@@ -60,14 +60,13 @@
         // Icon [3 | 4]
         // Large [6 | 8]
         val columns = 12
-        val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val stretchedTiles =
             remember(tiles) {
                 val sizedTiles =
                     tiles.map {
                         SizedTile(
                             it,
-                            if (iconTilesSpecs.contains(it.spec)) {
+                            if (iconTilesViewModel.isIconTile(it.spec)) {
                                 3
                             } else {
                                 6
@@ -81,7 +80,7 @@
             items(stretchedTiles.size, span = { GridItemSpan(stretchedTiles[it].width) }) { index ->
                 Tile(
                     tile = stretchedTiles[index].tile,
-                    iconOnly = iconTilesSpecs.contains(stretchedTiles[index].tile.spec),
+                    iconOnly = iconTilesViewModel.isIconTile(stretchedTiles[index].tile.spec),
                     modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
                 )
             }
@@ -95,12 +94,11 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit
     ) {
-        val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         DefaultEditTileGrid(
             tiles = tiles,
-            iconOnlySpecs = iconOnlySpecs,
+            isIconOnly = iconTilesViewModel::isIconTile,
             columns = GridCells.Fixed(columns),
             modifier = modifier,
             onAddTile = onAddTile,
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 a6838c0..f776bf0 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
@@ -165,7 +165,7 @@
 @Composable
 fun DefaultEditTileGrid(
     tiles: List<EditTileViewModel>,
-    iconOnlySpecs: Set<TileSpec>,
+    isIconOnly: (TileSpec) -> Boolean,
     columns: GridCells,
     modifier: Modifier,
     onAddTile: (TileSpec, Int) -> Unit,
@@ -176,8 +176,6 @@
     val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
         onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
     }
-    val isIconOnly: (TileSpec) -> Boolean =
-        remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
 
     TileLazyGrid(modifier = modifier, columns = columns) {
         // These Text are just placeholders to see the different sections. Not final UI.
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
index 9ad00c8..117c85c 100644
--- 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
@@ -20,14 +20,13 @@
 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>>
+    fun isIconTile(spec: TileSpec): Boolean
 }
 
 @SysUISingleton
-class IconTilesViewModelImpl @Inject constructor(interactor: IconTilesInteractor) :
+class IconTilesViewModelImpl @Inject constructor(private val interactor: IconTilesInteractor) :
     IconTilesViewModel {
-    override val iconTilesSpecs = interactor.iconTilesSpecs
+    override fun isIconTile(spec: TileSpec): Boolean = interactor.isIconTile(spec)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
index 214e9f0..24b80b8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt
@@ -158,6 +158,9 @@
     override suspend fun prependDefault(
         userId: Int,
     ) {
+        if (retailModeRepository.inRetailMode) {
+            return
+        }
         userTileRepositories.get(userId)?.prependDefault()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
index 8ad5cb2..aca8733 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/UserTileSpecRepository.kt
@@ -60,15 +60,21 @@
             _tiles =
                 changeEvents
                     .scan(loadTilesFromSettingsAndParse(userId)) { current, change ->
-                        change.apply(current).also {
-                            if (current != it) {
-                                if (change is RestoreTiles) {
-                                    logger.logTilesRestoredAndReconciled(current, it, userId)
-                                } else {
-                                    logger.logProcessTileChange(change, it, userId)
+                        change
+                            .apply(current)
+                            .also {
+                                if (current != it) {
+                                    if (change is RestoreTiles) {
+                                        logger.logTilesRestoredAndReconciled(current, it, userId)
+                                    } else {
+                                        logger.logProcessTileChange(change, it, userId)
+                                    }
                                 }
                             }
-                        }
+                            // Distinct preserves the order of the elements removing later
+                            // duplicates,
+                            // all tiles should be different
+                            .distinct()
                     }
                     .flowOn(backgroundDispatcher)
                     .stateIn(applicationScope)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index b7fcef4..97b5e87 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -43,6 +43,7 @@
 import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
 import com.android.systemui.qs.tiles.di.NewQSTileFactory
 import com.android.systemui.qs.toProto
+import com.android.systemui.retail.data.repository.RetailModeRepository
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.user.data.repository.UserRepository
 import com.android.systemui.util.kotlin.pairwise
@@ -137,6 +138,7 @@
     private val installedTilesComponentRepository: InstalledTilesComponentRepository,
     private val userRepository: UserRepository,
     private val minimumTilesRepository: MinimumTilesRepository,
+    private val retailModeRepository: RetailModeRepository,
     private val customTileStatePersister: CustomTileStatePersister,
     private val newQSTileFactory: Lazy<NewQSTileFactory>,
     private val tileFactory: QSFactory,
@@ -178,6 +180,14 @@
             installedTilesComponentRepository.getInstalledTilesComponents(it)
         }
 
+    private val minTiles: Int
+        get() =
+            if (retailModeRepository.inRetailMode) {
+                1
+            } else {
+                minimumTilesRepository.minNumberOfTiles
+            }
+
     init {
         if (featureFlags.pipelineEnabled) {
             startTileCollection()
@@ -273,7 +283,7 @@
                             newTileMap.filter { it.value is TileOrNotInstalled.NotInstalled }.keys,
                             newUser
                         )
-                        if (newResolvedTiles.size < minimumTilesRepository.minNumberOfTiles) {
+                        if (newResolvedTiles.size < minTiles) {
                             // We ended up with not enough tiles (some may be not installed).
                             // Prepend the default set of tiles
                             launch { tileSpecRepository.prependDefault(currentUser.value) }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index c6dfdd5..762dacd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -19,6 +19,7 @@
 import android.animation.ArgbEvaluator
 import android.animation.PropertyValuesHolder
 import android.animation.ValueAnimator
+import android.annotation.SuppressLint
 import android.content.Context
 import android.content.res.ColorStateList
 import android.content.res.Configuration
@@ -37,6 +38,7 @@
 import android.util.TypedValue
 import android.view.Gravity
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityEvent
@@ -332,6 +334,21 @@
     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
         super.onLayout(changed, l, t, r, b)
         updateHeight()
+        maybeUpdateLongPressEffectDimensions()
+    }
+
+    private fun maybeUpdateLongPressEffectDimensions() {
+        if (!isLongClickable || longPressEffect == null) return
+
+        val actualHeight = if (heightOverride != HeightOverrideable.NO_OVERRIDE) {
+            heightOverride
+        } else {
+            measuredHeight
+        }
+        initialLongPressProperties?.height = actualHeight.toFloat()
+        initialLongPressProperties?.width = measuredWidth.toFloat()
+        finalLongPressProperties?.height = LONG_PRESS_EFFECT_HEIGHT_SCALE * actualHeight
+        finalLongPressProperties?.width = LONG_PRESS_EFFECT_WIDTH_SCALE * measuredWidth
     }
 
     override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
@@ -380,15 +397,22 @@
 
     override fun init(tile: QSTile) {
         val expandable = Expandable.fromView(this)
-        init(
+        if (quickSettingsVisualHapticsLongpress()) {
+            isHapticFeedbackEnabled = false
+            longPressEffect?.qsTile = tile
+            longPressEffect?.expandable = expandable
+            init(
+                { _: View? -> longPressEffect?.onTileClick() },
+                null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
+            )
+        } else {
+            init(
                 { _: View? -> tile.click(expandable) },
                 { _: View? ->
                     tile.longClick(expandable)
                     true
-                }
-        )
-        if (quickSettingsVisualHapticsLongpress()) {
-            isHapticFeedbackEnabled = false // Haptics will be handled by the [QSLongPressEffect]
+                },
+            )
         }
     }
 
@@ -526,6 +550,20 @@
         return sb.toString()
     }
 
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        // let the View run the onTouch logic for click and long-click detection
+        val result = super.onTouchEvent(event)
+        if (longPressEffect != null) {
+            when (event?.actionMasked) {
+                MotionEvent.ACTION_DOWN -> longPressEffect.handleActionDown()
+                MotionEvent.ACTION_UP -> longPressEffect.handleActionUp()
+                MotionEvent.ACTION_CANCEL -> longPressEffect.handleActionCancel()
+            }
+        }
+        return result
+    }
+
     // HANDLE STATE CHANGES RELATED METHODS
 
     protected open fun handleStateChanged(state: QSTile.State) {
@@ -660,7 +698,6 @@
             // Long-press effects might have been enabled before but the new state does not
             // handle a long-press. In this case, we go back to the behaviour of a regular tile
             // and clean-up the resources
-            setOnTouchListener(null)
             unbindLongPressEffect()
             showRippleEffect = isClickable
             initialLongPressProperties = null
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 1d8b7e5b..bf0843b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -20,10 +20,12 @@
 import android.graphics.Rect
 import android.os.PowerManager
 import android.os.SystemClock
+import android.util.ArraySet
 import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.OnBackPressedDispatcherOwner
 import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
@@ -35,6 +37,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.compose.theme.PlatformTheme
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Flags.glanceableHubFullscreenSwipe
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
 import com.android.systemui.communal.dagger.Communal
@@ -52,10 +55,12 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.kotlin.BooleanFlowOperators.not
 import com.android.systemui.util.kotlin.collectFlow
+import java.util.function.Consumer
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
@@ -77,11 +82,38 @@
     private val communalColors: CommunalColors,
     private val ambientTouchComponentFactory: AmbientTouchComponent.Factory,
     private val communalContent: CommunalContent,
-    @Communal private val dataSourceDelegator: SceneDataSourceDelegator
+    @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
+    private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController,
 ) : LifecycleOwner {
+
+    private class CommunalWrapper(context: Context) : FrameLayout(context) {
+        private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
+
+        override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
+            consumers.forEach { it.accept(disallowIntercept) }
+            super.requestDisallowInterceptTouchEvent(disallowIntercept)
+        }
+
+        fun dispatchTouchEvent(
+            ev: MotionEvent?,
+            disallowInterceptConsumer: Consumer<Boolean>?
+        ): Boolean {
+            disallowInterceptConsumer?.apply { consumers.add(this) }
+
+            try {
+                return super.dispatchTouchEvent(ev)
+            } finally {
+                consumers.clear()
+            }
+        }
+    }
+
     /** The container view for the hub. This will not be initialized until [initView] is called. */
     private var communalContainerView: View? = null
 
+    /** Wrapper around the communal container to intercept touch events */
+    private var communalContainerWrapper: CommunalWrapper? = null
+
     /**
      * This lifecycle is used to control when the [touchMonitor] listens to touches. The lifecycle
      * should only be [Lifecycle.State.RESUMED] when the hub is showing and not covered by anything,
@@ -271,9 +303,13 @@
         )
         collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
 
-        communalContainerView = containerView
-
-        return containerView
+        if (glanceableHubFullscreenSwipe()) {
+            communalContainerWrapper = CommunalWrapper(containerView.context)
+            communalContainerWrapper?.addView(communalContainerView)
+            return communalContainerWrapper!!
+        } else {
+            return containerView
+        }
     }
 
     /**
@@ -306,6 +342,11 @@
             lifecycleRegistry.currentState = Lifecycle.State.CREATED
             communalContainerView = null
         }
+
+        communalContainerWrapper?.let {
+            (it.parent as ViewGroup).removeView(it)
+            communalContainerWrapper = null
+        }
     }
 
     /**
@@ -319,6 +360,18 @@
      */
     fun onTouchEvent(ev: MotionEvent): Boolean {
         SceneContainerFlag.assertInLegacyMode()
+
+        // In the case that we are handling full swipes on the lockscreen, are on the lockscreen,
+        // and the touch is within the horizontal notification band on the screen, do not process
+        // the touch.
+        if (
+            glanceableHubFullscreenSwipe() &&
+                !hubShowing &&
+                !notificationStackScrollLayoutController.isBelowLastNotification(ev.x, ev.y)
+        ) {
+            return false
+        }
+
         return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false
     }
 
@@ -330,12 +383,16 @@
         val hubOccluded = anyBouncerShowing || shadeShowing
 
         if (isDown && !hubOccluded) {
-            val x = ev.rawX
-            val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
-            if (inOpeningSwipeRegion || hubShowing) {
-                // Steal touch events when the hub is open, or if the touch started in the opening
-                // gesture region.
+            if (glanceableHubFullscreenSwipe()) {
                 isTrackingHubTouch = true
+            } else {
+                val x = ev.rawX
+                val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
+                if (inOpeningSwipeRegion || hubShowing) {
+                    // Steal touch events when the hub is open, or if the touch started in the
+                    // opening gesture region.
+                    isTrackingHubTouch = true
+                }
             }
         }
 
@@ -343,10 +400,7 @@
             if (isUp || isCancel) {
                 isTrackingHubTouch = false
             }
-            dispatchTouchEvent(view, ev)
-            // Return true regardless of dispatch result as some touches at the start of a gesture
-            // may return false from dispatchTouchEvent.
-            return true
+            return dispatchTouchEvent(view, ev)
         }
 
         return false
@@ -356,13 +410,30 @@
      * Dispatches the touch event to the communal container and sends a user activity event to reset
      * the screen timeout.
      */
-    private fun dispatchTouchEvent(view: View, ev: MotionEvent) {
-        view.dispatchTouchEvent(ev)
-        powerManager.userActivity(
-            SystemClock.uptimeMillis(),
-            PowerManager.USER_ACTIVITY_EVENT_TOUCH,
-            0
-        )
+    private fun dispatchTouchEvent(view: View, ev: MotionEvent): Boolean {
+        try {
+            var handled = false
+            if (glanceableHubFullscreenSwipe()) {
+                communalContainerWrapper?.dispatchTouchEvent(ev) {
+                    if (it) {
+                        handled = true
+                    }
+                }
+                return handled || hubShowing
+            } else {
+                view.dispatchTouchEvent(ev)
+                // Return true regardless of dispatch result as some touches at the start of a
+                // gesture
+                // may return false from dispatchTouchEvent.
+                return true
+            }
+        } finally {
+            powerManager.userActivity(
+                SystemClock.uptimeMillis(),
+                PowerManager.USER_ACTIVITY_EVENT_TOUCH,
+                0
+            )
+        }
     }
 
     override val lifecycle: Lifecycle
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 6df8ac4..4f6a64f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -65,6 +65,7 @@
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.classifier.Classifier;
+import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
 import com.android.systemui.dump.DumpManager;
@@ -157,6 +158,7 @@
     private final ShadeRepository mShadeRepository;
     private final ShadeInteractor mShadeInteractor;
     private final ActiveNotificationsInteractor mActiveNotificationsInteractor;
+    private final Lazy<CommunalTransitionViewModel> mCommunalTransitionViewModelLazy;
     private final JavaAdapter mJavaAdapter;
     private final FalsingManager mFalsingManager;
     private final AccessibilityManager mAccessibilityManager;
@@ -334,6 +336,7 @@
             JavaAdapter javaAdapter,
             CastController castController,
             SplitShadeStateController splitShadeStateController,
+            Lazy<CommunalTransitionViewModel> communalTransitionViewModelLazy,
             Lazy<LargeScreenHeaderHelper> largeScreenHeaderHelperLazy
     ) {
         SceneContainerFlag.assertInLegacyMode();
@@ -379,6 +382,7 @@
         mShadeRepository = shadeRepository;
         mShadeInteractor = shadeInteractor;
         mActiveNotificationsInteractor = activeNotificationsInteractor;
+        mCommunalTransitionViewModelLazy = communalTransitionViewModelLazy;
         mJavaAdapter = javaAdapter;
 
         mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback());
@@ -458,6 +462,9 @@
         initNotificationStackScrollLayoutController();
         mJavaAdapter.alwaysCollectFlow(
                 mShadeInteractor.isExpandToQsEnabled(), this::setExpansionEnabledPolicy);
+        mJavaAdapter.alwaysCollectFlow(
+                mCommunalTransitionViewModelLazy.get().isUmoOnCommunal(),
+                this::setShouldUpdateSquishinessOnMedia);
     }
 
     private void initNotificationStackScrollLayoutController() {
@@ -892,6 +899,12 @@
         }
     }
 
+    private void setShouldUpdateSquishinessOnMedia(boolean shouldUpdate) {
+        if (mQs != null) {
+            mQs.setShouldUpdateSquishinessOnMedia(shouldUpdate);
+        }
+    }
+
     void setOverScrollAmount(int overExpansion) {
         if (mQs != null) {
             mQs.setOverScrollAmount(overExpansion);
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 5e4b173..a171d33 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
@@ -381,7 +381,10 @@
         mLogger.logHandleUpdateCarrierInfo(info);
 
         mNoSimTextView.setVisibility(View.GONE);
-        if (!info.airplaneMode && info.anySimReady) {
+        if (info.isInSatelliteMode) {
+            mLogger.logUsingSatelliteText(info.carrierText);
+            showSingleText(info.carrierText);
+        } else if (!info.airplaneMode && info.anySimReady) {
             boolean[] slotSeen = new boolean[SIM_SLOTS];
             if (info.listOfCarriers.length == info.subscriptionIds.length) {
                 mLogger.logUsingSimViews();
@@ -416,22 +419,31 @@
                         info.listOfCarriers.length, info.subscriptionIds.length);
             }
         } else {
+            // No sims or airplane mode (but not WFC), so just show the main carrier text.
             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++) {
-                mInfos[i] = mInfos[i].changeVisibility(false);
-                mCarrierGroups[i].setCarrierText("");
-                mCarrierGroups[i].setVisibility(View.GONE);
-            }
-            mNoSimTextView.setText(info.carrierText);
-            if (!TextUtils.isEmpty(info.carrierText)) {
-                mNoSimTextView.setVisibility(View.VISIBLE);
-            }
+            showSingleText(info.carrierText);
         }
         handleUpdateState(); // handleUpdateCarrierInfo is always called from main thread.
     }
 
+    /**
+     * Shows only the given text in a single TextView and hides ShadeCarrierGroup (which would show
+     * individual SIM details).
+     */
+    private void showSingleText(CharSequence text) {
+        for (int i = 0; i < SIM_SLOTS; i++) {
+            mInfos[i] = mInfos[i].changeVisibility(false);
+            mCarrierGroups[i].setCarrierText("");
+            mCarrierGroups[i].setVisibility(View.GONE);
+        }
+        // TODO(b/341841138): Re-name this view now that it's being used for more than just the
+        //  no-SIM case.
+        mNoSimTextView.setText(text);
+        if (!TextUtils.isEmpty(text)) {
+            mNoSimTextView.setVisibility(View.VISIBLE);
+        }
+    }
+
     private static class H extends Handler {
         private Consumer<CarrierTextManager.CarrierTextCallbackInfo> mUpdateCarrierInfo;
         private Runnable mUpdateState;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt
index af06a35..b563cd9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt
@@ -65,6 +65,15 @@
         )
     }
 
+    fun logUsingSatelliteText(text: CharSequence) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { str1 = "$text" },
+            { "â”— updating No SIM view with satellite text=$str1" },
+        )
+    }
+
     fun logUsingSimViews() {
         buffer.log(TAG, LogLevel.VERBOSE, {}, { "â”— updating SIM views" })
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 9885fe4..e7fc18e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.keyguard.LockIconViewController
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
@@ -30,7 +31,8 @@
 class ShadeLockscreenInteractorImpl
 @Inject
 constructor(
-    @Background private val scope: CoroutineScope,
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundScope: CoroutineScope,
     private val shadeInteractor: ShadeInteractor,
     private val sceneInteractor: SceneInteractor,
     private val lockIconViewController: LockIconViewController,
@@ -68,7 +70,7 @@
         // Now handled elsewhere. Do nothing.
     }
     override fun transitionToExpandedShade(delay: Long) {
-        scope.launch {
+        backgroundScope.launch {
             delay(delay)
             changeToShadeScene()
         }
@@ -96,10 +98,12 @@
     }
 
     private fun changeToShadeScene() {
-        val shadeMode = shadeInteractor.shadeMode.value
-        sceneInteractor.changeScene(
-            if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
-            "ShadeLockscreenInteractorImpl.expandToNotifications",
-        )
+        applicationScope.launch {
+            val shadeMode = shadeInteractor.shadeMode.value
+            sceneInteractor.changeScene(
+                if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade,
+                "ShadeLockscreenInteractorImpl.expandToNotifications",
+            )
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
index d00916a..c742f641 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java
@@ -610,7 +610,7 @@
             keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                     mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
                     calendarIcon,
-                    KeyEvent.KEYCODE_L,
+                    KeyEvent.KEYCODE_K,
                     KeyEvent.META_META_ON));
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 3cf61e2..8d3f728 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -362,20 +362,34 @@
         }
 
         protected boolean hasSameIcon(Object parentData, Object childData) {
-            Icon parentIcon = ((Notification) parentData).getSmallIcon();
-            Icon childIcon = ((Notification) childData).getSmallIcon();
+            Icon parentIcon = getIcon((Notification) parentData);
+            Icon childIcon = getIcon((Notification) childData);
             return parentIcon.sameAs(childIcon);
         }
 
+        private static Icon getIcon(Notification notification) {
+            if (notification.shouldUseAppIcon()) {
+                return notification.getAppIcon();
+            }
+            return notification.getSmallIcon();
+        }
+
         /**
          * @return whether two ImageViews have the same colorFilterSet or none at all
          */
         protected boolean hasSameColor(Object parentData, Object childData) {
-            int parentColor = ((Notification) parentData).color;
-            int childColor = ((Notification) childData).color;
+            int parentColor = getColor((Notification) parentData);
+            int childColor = getColor((Notification) childData);
             return parentColor == childColor;
         }
 
+        private static int getColor(Notification notification) {
+            if (notification.shouldUseAppIcon()) {
+                return 0;  // the color filter isn't applied if using the app icon
+            }
+            return notification.color;
+        }
+
         @Override
         public boolean isEmpty(View view) {
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 854ef92..bb26f92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar;
 
 import static android.app.Flags.keyguardPrivateNotifications;
+import static android.app.Flags.redactSensitiveContentNotificationsOnLockscreen;
 import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
 import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED;
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
@@ -654,10 +655,12 @@
                 !userAllowsPrivateNotificationsInPublic(mCurrentUserId);
         boolean isNotifForManagedProfile = mCurrentManagedProfiles.contains(userId);
         boolean isNotifUserRedacted = !userAllowsPrivateNotificationsInPublic(userId);
+        boolean isNotifSensitive = redactSensitiveContentNotificationsOnLockscreen()
+                && ent.getRanking() != null && ent.getRanking().hasSensitiveContent();
 
-        // redact notifications if the current user is redacting notifications; however if the
-        // notification is associated with a managed profile, we rely on the managed profile
-        // setting to determine whether to redact it
+        // redact notifications if the current user is redacting notifications or the notification
+        // contains sensitive content. However if the notification is associated with a managed
+        // profile, we rely on the managed profile setting to determine whether to redact it.
         boolean isNotifRedacted = (!isNotifForManagedProfile && isCurrentUserRedactingNotifs)
                 || isNotifUserRedacted;
 
@@ -666,10 +669,11 @@
         boolean userForcesRedaction = packageHasVisibilityOverride(ent.getSbn().getKey());
 
         if (keyguardPrivateNotifications()) {
-            return !mKeyguardAllowingNotifications
-                    || userForcesRedaction || notificationRequestsRedaction && isNotifRedacted;
+            return !mKeyguardAllowingNotifications || isNotifSensitive
+                    || userForcesRedaction || (notificationRequestsRedaction && isNotifRedacted);
         } else {
-            return userForcesRedaction || notificationRequestsRedaction && isNotifRedacted;
+            return userForcesRedaction || isNotifSensitive
+                    || (notificationRequestsRedaction && isNotifRedacted);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 455c964..2e87a5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -143,6 +143,12 @@
     private var managedUserHandle: UserHandle? = null
     private var mSplitShadeEnabled = false
 
+    var suppressDisconnects = false
+        set(value) {
+            field = value
+            disconnect()
+        }
+
     // TODO(b/202758428): refactor so that we can test color updates via region samping, similar to
     //  how we test color updates when theme changes (See testThemeChangeUpdatesTextColor).
 
@@ -522,6 +528,7 @@
      */
     fun disconnect() {
         if (!smartspaceViews.isEmpty()) return
+        if (suppressDisconnects) return
 
         execution.assertIsMainThread()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index c643238..682a9ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -34,8 +34,8 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
-import com.android.systemui.statusbar.notification.row.NotificationContentInflaterLogger
 import com.android.systemui.statusbar.notification.row.NotificationContentView
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinderLogger
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
@@ -44,30 +44,29 @@
 import javax.inject.Inject
 
 /** Populates additional information in conversation notifications */
-class ConversationNotificationProcessor @Inject constructor(
+class ConversationNotificationProcessor
+@Inject
+constructor(
     private val launcherApps: LauncherApps,
     private val conversationNotificationManager: ConversationNotificationManager
 ) {
     fun processNotification(
-            entry: NotificationEntry,
-            recoveredBuilder: Notification.Builder,
-            logger: NotificationContentInflaterLogger
+        entry: NotificationEntry,
+        recoveredBuilder: Notification.Builder,
+        logger: NotificationRowContentBinderLogger
     ): Notification.MessagingStyle? {
         val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null
         messagingStyle.conversationType =
-                if (entry.ranking.channel.isImportantConversation)
-                    Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
-                else
-                    Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
+            if (entry.ranking.channel.isImportantConversation)
+                Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
+            else Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
         entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
             logger.logAsyncTaskProgress(entry, "getting shortcut icon")
             messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
-            shortcutInfo.label?.let { label ->
-                messagingStyle.conversationTitle = label
-            }
+            shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }
         }
         messagingStyle.unreadMessageCount =
-                conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
+            conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
         return messagingStyle
     }
 }
@@ -77,7 +76,9 @@
  * animations to conserve CPU and memory.
  */
 @SysUISingleton
-class AnimatedImageNotificationManager @Inject constructor(
+class AnimatedImageNotificationManager
+@Inject
+constructor(
     private val notifCollection: CommonNotifCollection,
     private val bindEventManager: BindEventManager,
     private val headsUpManager: HeadsUpManager,
@@ -88,17 +89,21 @@
 
     /** Begins listening to state changes and updating animations accordingly. */
     fun bind() {
-        headsUpManager.addListener(object : OnHeadsUpChangedListener {
-            override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
-                updateAnimatedImageDrawables(entry)
+        headsUpManager.addListener(
+            object : OnHeadsUpChangedListener {
+                override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
+                    updateAnimatedImageDrawables(entry)
+                }
             }
-        })
-        statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
-            override fun onExpandedChanged(isExpanded: Boolean) {
-                isStatusBarExpanded = isExpanded
-                notifCollection.allNotifs.forEach(::updateAnimatedImageDrawables)
+        )
+        statusBarStateController.addCallback(
+            object : StatusBarStateController.StateListener {
+                override fun onExpandedChanged(isExpanded: Boolean) {
+                    isStatusBarExpanded = isExpanded
+                    notifCollection.allNotifs.forEach(::updateAnimatedImageDrawables)
+                }
             }
-        })
+        )
         bindEventManager.addListener(::updateAnimatedImageDrawables)
     }
 
@@ -108,74 +113,73 @@
         }
 
     private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) =
-            (row.layouts?.asSequence() ?: emptySequence())
-                    .flatMap { layout -> layout.allViews.asSequence() }
-                    .flatMap { view ->
-                        (view as? ConversationLayout)?.messagingGroups?.asSequence()
-                                ?: (view as? MessagingLayout)?.messagingGroups?.asSequence()
-                                ?: emptySequence()
-                    }
-                    .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
-                    .mapNotNull { view ->
-                        (view as? MessagingImageMessage)
-                                ?.let { imageMessage ->
-                                    imageMessage.drawable as? AnimatedImageDrawable
-                                }
-                    }
-                    .forEach { animatedImageDrawable ->
-                        if (animating) animatedImageDrawable.start()
-                        else animatedImageDrawable.stop()
-                    }
+        (row.layouts?.asSequence() ?: emptySequence())
+            .flatMap { layout -> layout.allViews.asSequence() }
+            .flatMap { view ->
+                (view as? ConversationLayout)?.messagingGroups?.asSequence()
+                    ?: (view as? MessagingLayout)?.messagingGroups?.asSequence() ?: emptySequence()
+            }
+            .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
+            .mapNotNull { view ->
+                (view as? MessagingImageMessage)?.let { imageMessage ->
+                    imageMessage.drawable as? AnimatedImageDrawable
+                }
+            }
+            .forEach { animatedImageDrawable ->
+                if (animating) animatedImageDrawable.start() else animatedImageDrawable.stop()
+            }
 }
 
 /**
  * Tracks state related to conversation notifications, and updates the UI of existing notifications
  * when necessary.
+ *
  * TODO(b/214083332) Refactor this class to use the right coordinators and controllers
  */
 @SysUISingleton
-class ConversationNotificationManager @Inject constructor(
+class ConversationNotificationManager
+@Inject
+constructor(
     bindEventManager: BindEventManager,
     private val context: Context,
     private val notifCollection: CommonNotifCollection,
     @Main private val mainHandler: Handler
 ) {
     // Need this state to be thread safe, since it's accessed from the ui thread
-    // (NotificationEntryListener) and a bg thread (NotificationContentInflater)
+    // (NotificationEntryListener) and a bg thread (NotificationRowContentBinder)
     private val states = ConcurrentHashMap<String, ConversationState>()
 
     private var notifPanelCollapsed = true
 
     private fun updateNotificationRanking(rankingMap: RankingMap) {
         fun getLayouts(view: NotificationContentView) =
-                sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
+            sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
         val ranking = Ranking()
-        val activeConversationEntries = states.keys.asSequence()
-                .mapNotNull { notifCollection.getEntry(it) }
+        val activeConversationEntries =
+            states.keys.asSequence().mapNotNull { notifCollection.getEntry(it) }
         for (entry in activeConversationEntries) {
             if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) {
                 val important = ranking.channel.isImportantConversation
                 var changed = false
-                entry.row?.layouts?.asSequence()
-                        ?.flatMap(::getLayouts)
-                        ?.mapNotNull { it as? ConversationLayout }
-                        ?.filterNot { it.isImportantConversation == important }
-                        ?.forEach { layout ->
-                            changed = true
-                            if (important && entry.isMarkedForUserTriggeredMovement) {
-                                // delay this so that it doesn't animate in until after
-                                // the notif has been moved in the shade
-                                mainHandler.postDelayed(
-                                        {
-                                            layout.setIsImportantConversation(
-                                                    important,
-                                                    true)
-                                        },
-                                        IMPORTANCE_ANIMATION_DELAY.toLong())
-                            } else {
-                                layout.setIsImportantConversation(important, false)
-                            }
+                entry.row
+                    ?.layouts
+                    ?.asSequence()
+                    ?.flatMap(::getLayouts)
+                    ?.mapNotNull { it as? ConversationLayout }
+                    ?.filterNot { it.isImportantConversation == important }
+                    ?.forEach { layout ->
+                        changed = true
+                        if (important && entry.isMarkedForUserTriggeredMovement) {
+                            // delay this so that it doesn't animate in until after
+                            // the notif has been moved in the shade
+                            mainHandler.postDelayed(
+                                { layout.setIsImportantConversation(important, true) },
+                                IMPORTANCE_ANIMATION_DELAY.toLong()
+                            )
+                        } else {
+                            layout.setIsImportantConversation(important, false)
                         }
+                    }
             }
         }
     }
@@ -192,9 +196,7 @@
         }
         entry.row?.setOnExpansionChangedListener { isExpanded ->
             if (entry.row?.isShown == true && isExpanded) {
-                entry.row.performOnIntrinsicHeightReached {
-                    updateCount(isExpanded)
-                }
+                entry.row.performOnIntrinsicHeightReached { updateCount(isExpanded) }
             } else {
                 updateCount(isExpanded)
             }
@@ -203,31 +205,38 @@
     }
 
     init {
-        notifCollection.addCollectionListener(object : NotifCollectionListener {
-            override fun onRankingUpdate(ranking: RankingMap) =
-                updateNotificationRanking(ranking)
+        notifCollection.addCollectionListener(
+            object : NotifCollectionListener {
+                override fun onRankingUpdate(ranking: RankingMap) =
+                    updateNotificationRanking(ranking)
 
-            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
-                removeTrackedEntry(entry)
-        })
+                override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
+                    removeTrackedEntry(entry)
+            }
+        )
         bindEventManager.addListener(::onEntryViewBound)
     }
 
     private fun ConversationState.shouldIncrementUnread(newBuilder: Notification.Builder) =
-            if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) {
-                false
-            } else {
-                val oldBuilder = Notification.Builder.recoverBuilder(context, notification)
-                Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder)
-            }
+        if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) {
+            false
+        } else {
+            val oldBuilder = Notification.Builder.recoverBuilder(context, notification)
+            Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder)
+        }
 
     fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int =
-            states.compute(entry.key) { _, state ->
-                val newCount = state?.run {
-                    if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1 else unreadCount
-                } ?: 1
+        states
+            .compute(entry.key) { _, state ->
+                val newCount =
+                    state?.run {
+                        if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1
+                        else unreadCount
+                    }
+                        ?: 1
                 ConversationState(newCount, entry.sbn.notification)
-            }!!.unreadCount
+            }!!
+            .unreadCount
 
     fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) {
         notifPanelCollapsed = isCollapsed
@@ -235,18 +244,17 @@
 
         // When the notification panel is expanded, reset the counters of any expanded
         // conversations
-        val expanded = states
+        val expanded =
+            states
                 .asSequence()
                 .mapNotNull { (key, _) ->
                     notifCollection.getEntry(key)?.let { entry ->
-                        if (entry.row?.isExpanded == true) key to entry
-                        else null
+                        if (entry.row?.isExpanded == true) key to entry else null
                     }
                 }
                 .toMap()
         states.replaceAll { key, state ->
-            if (expanded.contains(key)) state.copy(unreadCount = 0)
-            else state
+            if (expanded.contains(key)) state.copy(unreadCount = 0) else state
         }
         // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the
         // lambda if threads are in contention.
@@ -262,16 +270,16 @@
     }
 
     private fun resetBadgeUi(row: ExpandableNotificationRow): Unit =
-            (row.layouts?.asSequence() ?: emptySequence())
-                    .flatMap { layout -> layout.allViews.asSequence() }
-                    .mapNotNull { view -> view as? ConversationLayout }
-                    .forEach { convoLayout -> convoLayout.setUnreadCount(0) }
+        (row.layouts?.asSequence() ?: emptySequence())
+            .flatMap { layout -> layout.allViews.asSequence() }
+            .mapNotNull { view -> view as? ConversationLayout }
+            .forEach { convoLayout -> convoLayout.setUnreadCount(0) }
 
     private data class ConversationState(val unreadCount: Int, val notification: Notification)
 
     companion object {
         private const val IMPORTANCE_ANIMATION_DELAY =
-                StackStateAnimator.ANIMATION_DURATION_STANDARD +
+            StackStateAnimator.ANIMATION_DURATION_STANDARD +
                 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE +
                 100
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java
index 57b41f3..cafe6ffc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicChildBindController.java
@@ -16,8 +16,8 @@
 
 package com.android.systemui.statusbar.notification;
 
-import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_CONTRACTED;
-import static com.android.systemui.statusbar.notification.row.NotificationContentInflater.FLAG_CONTENT_VIEW_EXPANDED;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
 
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
index 98109f9..fc47dc1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
@@ -23,7 +23,7 @@
 import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
-import com.android.systemui.statusbar.notification.row.NotificationContentInflater;
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder;
 
 import javax.inject.Inject;
 
@@ -100,9 +100,9 @@
         requireBinder().releaseViews(entry);
     }
 
-    private NotificationContentInflater.InflationCallback wrapInflationCallback(
+    private NotificationRowContentBinder.InflationCallback wrapInflationCallback(
             InflationCallback callback) {
-        return new NotificationContentInflater.InflationCallback() {
+        return new NotificationRowContentBinder.InflationCallback() {
             @Override
             public void handleInflationException(
                     NotificationEntry entry,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index 42bf4e7..071192b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -289,8 +289,9 @@
             // for each change, lookup the new value
             .map {
                 secureSettings.getIntForUser(
-                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
-                    UserHandle.USER_CURRENT,
+                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    def = 0,
+                    userHandle = UserHandle.USER_CURRENT,
                 ) == 1
             }
             // don't emit anything if nothing has changed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
index 319b499..16d0cc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt
@@ -18,25 +18,27 @@
 
 import android.app.Notification
 import android.content.Context
+import android.graphics.drawable.Drawable
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.contentDescForNotification
 import javax.inject.Inject
 
-/**
- * Testable wrapper around Context.
- */
-class IconBuilder @Inject constructor(
-    private val context: Context
-) {
+/** Testable wrapper around Context. */
+class IconBuilder @Inject constructor(private val context: Context) {
     fun createIconView(entry: NotificationEntry): StatusBarIconView {
         return StatusBarIconView(
-                context,
-                "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}",
-                entry.sbn)
+            context,
+            "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}",
+            entry.sbn
+        )
     }
 
     fun getIconContentDescription(n: Notification): CharSequence {
         return contentDescForNotification(context, n)
     }
+
+    fun getAppIcon(n: Notification): Drawable {
+        return n.loadHeaderAppIcon(context)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index 271b0a8..3df9374 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -20,6 +20,8 @@
 import android.app.Notification.MessagingStyle
 import android.app.Person
 import android.content.pm.LauncherApps
+import android.graphics.drawable.AdaptiveIconDrawable
+import android.graphics.drawable.Drawable
 import android.graphics.drawable.Icon
 import android.os.Build
 import android.os.Bundle
@@ -165,7 +167,7 @@
                 Log.wtf(
                     TAG,
                     "Updating using the cache is not supported when the " +
-                        "notifications_background_conversation_icons flag is off"
+                        "notifications_background_icons flag is off"
                 )
             }
             if (!usingCache || !Flags.notificationsBackgroundIcons()) {
@@ -216,39 +218,85 @@
 
     @Throws(InflationException::class)
     private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon {
-        val n = entry.sbn.notification
         val showPeopleAvatar = !redact && isImportantConversation(entry)
 
+        // If the descriptor is already cached, return it
+        getCachedIconDescriptor(entry, showPeopleAvatar)?.also {
+            return it
+        }
+
+        val n = entry.sbn.notification
+        var usingMonochromeAppIcon = false
+        val icon: Icon?
+        if (showPeopleAvatar) {
+            icon = createPeopleAvatar(entry)
+        } else if (android.app.Flags.notificationsUseMonochromeAppIcon()) {
+            if (n.shouldUseAppIcon()) {
+                icon =
+                    getMonochromeAppIcon(entry)?.also { usingMonochromeAppIcon = true }
+                        ?: n.smallIcon
+            } else {
+                icon = n.smallIcon
+            }
+        } else {
+            icon = n.smallIcon
+        }
+
+        if (icon == null) {
+            throw InflationException("No icon in notification from ${entry.sbn.packageName}")
+        }
+
+        val sbi = icon.toStatusBarIcon(entry)
+        cacheIconDescriptor(entry, sbi, showPeopleAvatar, usingMonochromeAppIcon)
+        return sbi
+    }
+
+    private fun getCachedIconDescriptor(
+        entry: NotificationEntry,
+        showPeopleAvatar: Boolean
+    ): StatusBarIcon? {
         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
+        val appIconDescriptor = entry.icons.appIconDescriptor
         val smallIconDescriptor = entry.icons.smallIconDescriptor
 
         // If cached, return corresponding cached values
-        if (showPeopleAvatar && peopleAvatarDescriptor != null) {
-            return peopleAvatarDescriptor
-        } else if (!showPeopleAvatar && smallIconDescriptor != null) {
-            return smallIconDescriptor
+        return when {
+            showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor
+            android.app.Flags.notificationsUseMonochromeAppIcon() && appIconDescriptor != null ->
+                appIconDescriptor
+            smallIconDescriptor != null -> smallIconDescriptor
+            else -> null
         }
+    }
 
-        val icon =
-            (if (showPeopleAvatar) {
-                createPeopleAvatar(entry)
-            } else {
-                n.smallIcon
-            })
-                ?: throw InflationException("No icon in notification from " + entry.sbn.packageName)
-
-        val sbi = icon.toStatusBarIcon(entry)
-
-        // Cache if important conversation or app icon.
-        if (isImportantConversation(entry) || android.app.Flags.notificationsUseAppIcon()) {
+    private fun cacheIconDescriptor(
+        entry: NotificationEntry,
+        descriptor: StatusBarIcon,
+        showPeopleAvatar: Boolean,
+        usingMonochromeAppIcon: Boolean
+    ) {
+        if (android.app.Flags.notificationsUseAppIcon() ||
+            android.app.Flags.notificationsUseMonochromeAppIcon()
+        ) {
+            // If either of the new icon flags is enabled, we cache the icon all the time.
             if (showPeopleAvatar) {
-                entry.icons.peopleAvatarDescriptor = sbi
+                entry.icons.peopleAvatarDescriptor = descriptor
+            } else if (usingMonochromeAppIcon) {
+                // When notificationsUseMonochromeAppIcon is enabled, we use the appIconDescriptor.
+                entry.icons.appIconDescriptor = descriptor
             } else {
-                entry.icons.smallIconDescriptor = sbi
+                // When notificationsUseAppIcon is enabled, the app icon overrides the small icon.
+                // But either way, it's a good idea to cache the descriptor.
+                entry.icons.smallIconDescriptor = descriptor
+            }
+        } else if (isImportantConversation(entry)) {
+            // Old approach: cache only if important conversation.
+            if (showPeopleAvatar) {
+                entry.icons.peopleAvatarDescriptor = descriptor
+            } else {
+                entry.icons.smallIconDescriptor = descriptor
             }
         }
-
-        return sbi
     }
 
     @Throws(InflationException::class)
@@ -276,6 +324,29 @@
         )
     }
 
+    // TODO(b/335211019): Should we merge this with the method in GroupHelper?
+    private fun getMonochromeAppIcon(entry: NotificationEntry): Icon? {
+        // TODO(b/335211019): This should be done in the background.
+        var monochromeIcon: Icon? = null
+        try {
+            val appIcon: Drawable = iconBuilder.getAppIcon(entry.sbn.notification)
+            if (appIcon is AdaptiveIconDrawable) {
+                if (appIcon.monochrome != null) {
+                    monochromeIcon =
+                        Icon.createWithResourceAdaptiveDrawable(
+                            /* resPackage = */ entry.sbn.packageName,
+                            /* resId = */ appIcon.sourceDrawableResId,
+                            /* useMonochrome = */ true,
+                            /* inset = */ -3.0f * AdaptiveIconDrawable.getExtraInsetFraction()
+                        )
+                }
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to getAppIcon() in getMonochromeAppIcon()", e)
+        }
+        return monochromeIcon
+    }
+
     private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) =
         withContext(bgCoroutineContext) {
             var icon: Icon? = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
index 442c097..d029ce7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java
@@ -33,6 +33,7 @@
     @Nullable private final StatusBarIconView mAodIcon;
 
     @Nullable private StatusBarIcon mSmallIconDescriptor;
+    @Nullable private StatusBarIcon mAppIconDescriptor;
     @Nullable private StatusBarIcon mPeopleAvatarDescriptor;
 
     private boolean mIsImportantConversation;
@@ -111,6 +112,15 @@
         mPeopleAvatarDescriptor = peopleAvatarDescriptor;
     }
 
+    @Nullable
+    StatusBarIcon getAppIconDescriptor() {
+        return mAppIconDescriptor;
+    }
+
+    void setAppIconDescriptor(@Nullable StatusBarIcon appIconDescriptor) {
+        mAppIconDescriptor = appIconDescriptor;
+    }
+
     boolean isImportantConversation() {
         return mIsImportantConversation;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt
index c74c396..c29d700 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/AvalancheProvider.kt
@@ -21,9 +21,9 @@
 import android.content.Intent
 import android.content.IntentFilter
 import android.util.Log
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 
 // Class to track avalanche trigger event time.
@@ -33,6 +33,7 @@
 constructor(
         private val broadcastDispatcher: BroadcastDispatcher,
         private val logger: VisualInterruptionDecisionLogger,
+        private val uiEventLogger: UiEventLogger,
 ) {
     val TAG = "AvalancheProvider"
     val timeoutMs = 120000
@@ -56,6 +57,7 @@
                     return
                 }
                 Log.d(TAG, "broadcastReceiver received intent.action=" + intent.action)
+                uiEventLogger.log(AvalancheSuppressor.AvalancheEvent.START);
                 startTime = System.currentTimeMillis()
             }
         }
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 938a71f..42a5bdf 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
@@ -33,6 +33,8 @@
 import android.provider.Settings
 import android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED
 import android.provider.Settings.Global.HEADS_UP_OFF
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEvent;
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.settings.UserTracker
@@ -241,12 +243,12 @@
     override fun shouldSuppress(entry: NotificationEntry) =
         keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
 }
-
 class AvalancheSuppressor(
     private val avalancheProvider: AvalancheProvider,
     private val systemClock: SystemClock,
     private val systemSettings: SystemSettings,
     private val packageManager: PackageManager,
+    private val uiEventLogger: UiEventLogger,
 ) :
     VisualInterruptionFilter(
         types = setOf(PEEK, PULSE),
@@ -266,6 +268,44 @@
         SUPPRESS
     }
 
+     enum class AvalancheEvent(private val id: Int) : UiEventLogger.UiEventEnum {
+         @UiEvent(doc = "An avalanche event occurred but this notification was suppressed by a " +
+                 "non-avalanche suppressor.")
+         START(1802),
+
+         @UiEvent(doc = "HUN was suppressed in avalanche.")
+         SUPPRESS(1803),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is high priority.")
+         ALLOW_CONVERSATION_AFTER_AVALANCHE(1804),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is a high priority conversation.")
+         ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME(1805),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is a call.")
+         ALLOW_CALLSTYLE(1806),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.")
+         ALLOW_CATEGORY_REMINDER(1807),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is a calendar notification.")
+         ALLOW_CATEGORY_EVENT(1808),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it has a full screen intent and " +
+                 "the full screen intent permission is granted.")
+         ALLOW_FSI_WITH_PERMISSION_ON(1809),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is colorized.")
+         ALLOW_COLORIZED(1810),
+
+         @UiEvent(doc = "HUN allowed during avalanche because it is an emergency notification.")
+         ALLOW_EMERGENCY(1811);
+
+        override fun getId(): Int {
+            return id
+        }
+    }
+
     override fun shouldSuppress(entry: NotificationEntry): Boolean {
         if (!isCooldownEnabled()) {
             return false
@@ -287,41 +327,46 @@
             entry.ranking.isConversation &&
                 entry.sbn.notification.getWhen() > avalancheProvider.startTime
         ) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_CONVERSATION_AFTER_AVALANCHE)
             return State.ALLOW_CONVERSATION_AFTER_AVALANCHE
         }
 
         if (entry.channel?.isImportantConversation == true) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME)
             return State.ALLOW_HIGH_PRIORITY_CONVERSATION_ANY_TIME
         }
 
         if (entry.sbn.notification.isStyle(Notification.CallStyle::class.java)) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_CALLSTYLE)
             return State.ALLOW_CALLSTYLE
         }
 
         if (entry.sbn.notification.category == CATEGORY_REMINDER) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_CATEGORY_REMINDER)
             return State.ALLOW_CATEGORY_REMINDER
         }
 
         if (entry.sbn.notification.category == CATEGORY_EVENT) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_CATEGORY_EVENT)
             return State.ALLOW_CATEGORY_EVENT
         }
 
         if (entry.sbn.notification.fullScreenIntent != null) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_FSI_WITH_PERMISSION_ON)
             return State.ALLOW_FSI_WITH_PERMISSION_ON
         }
-
         if (entry.sbn.notification.isColorized) {
-            return State.ALLOW_COLORIZED
-        }
-        if (entry.sbn.notification.isColorized) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_COLORIZED)
             return State.ALLOW_COLORIZED
         }
         if (
             packageManager.checkPermission(RECEIVE_EMERGENCY_BROADCAST, entry.sbn.packageName) ==
                 PERMISSION_GRANTED
         ) {
+            uiEventLogger.log(AvalancheEvent.ALLOW_EMERGENCY)
             return State.ALLOW_EMERGENCY
         }
+        uiEventLogger.log(AvalancheEvent.SUPPRESS)
         return State.SUPPRESS
     }
 
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 7e16cd5..84f8662 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
@@ -178,7 +178,8 @@
 
         if (NotificationAvalancheSuppression.isEnabled) {
             addFilter(
-                AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+                AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                        uiEventLogger)
             )
             avalancheProvider.register()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
index 0fdf681..1efa56f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt
@@ -20,8 +20,6 @@
 import android.util.Log
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import dagger.Lazy
 import javax.inject.Inject
 
@@ -30,7 +28,6 @@
 class NotificationMemoryMonitor
 @Inject
 constructor(
-    private val featureFlags: FeatureFlags,
     private val notificationMemoryDumper: NotificationMemoryDumper,
     private val notificationMemoryLogger: Lazy<NotificationMemoryLogger>,
 ) : CoreStartable {
@@ -42,9 +39,6 @@
     override fun start() {
         Log.d(TAG, "NotificationMemoryMonitor initialized.")
         notificationMemoryDumper.init()
-        if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_LOGGING_ENABLED)) {
-            Log.d(TAG, "Notification memory logging enabled.")
-            notificationMemoryLogger.get().init()
-        }
+        notificationMemoryLogger.get().init()
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 99ce454..190a2cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -548,7 +548,7 @@
      * and ensuring that the view is freed when it is safe to remove.
      *
      * @param inflationFlag flag corresponding to the content view to be freed
-     * @deprecated By default, {@link NotificationContentInflater#unbindContent} will tell the
+     * @deprecated By default, {@link NotificationRowContentBinder#unbindContent} will tell the
      * view hierarchy to only free when the view is safe to remove so this method is no longer
      * needed. Will remove when all uses are gone.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 2f03871..6ba26d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -92,7 +92,7 @@
     private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider;
     private final HeadsUpStyleProvider mHeadsUpStyleProvider;
 
-    private final NotificationContentInflaterLogger mLogger;
+    private final NotificationRowContentBinderLogger mLogger;
 
     @Inject
     NotificationContentInflater(
@@ -104,7 +104,7 @@
             SmartReplyStateInflater smartRepliesInflater,
             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
             HeadsUpStyleProvider headsUpStyleProvider,
-            NotificationContentInflaterLogger logger) {
+            NotificationRowContentBinderLogger logger) {
         mRemoteViewCache = remoteViewCache;
         mRemoteInputManager = remoteInputManager;
         mConversationProcessor = conversationProcessor;
@@ -345,7 +345,7 @@
             Context packageContext,
             InflatedSmartReplyState previousSmartReplyState,
             SmartReplyStateInflater inflater,
-            NotificationContentInflaterLogger logger) {
+            NotificationRowContentBinderLogger logger) {
         boolean inflateContracted = (reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0
                 && result.newContentView != null;
         boolean inflateExpanded = (reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0
@@ -377,7 +377,7 @@
             ExpandableNotificationRow row,
             NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
             HeadsUpStyleProvider headsUpStyleProvider,
-            NotificationContentInflaterLogger logger) {
+            NotificationRowContentBinderLogger logger) {
         return TraceUtils.trace("NotificationContentInflater.createRemoteViews", () -> {
             InflationProgress result = new InflationProgress();
             final NotificationEntry entryForLogging = row.getEntry();
@@ -465,7 +465,7 @@
             ExpandableNotificationRow row,
             RemoteViews.InteractionHandler remoteViewClickHandler,
             @Nullable InflationCallback callback,
-            NotificationContentInflaterLogger logger) {
+            NotificationRowContentBinderLogger logger) {
         Trace.beginAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
 
         NotificationContentView privateLayout = row.getPrivateLayout();
@@ -676,7 +676,7 @@
             NotificationViewWrapper existingWrapper,
             final HashMap<Integer, CancellationSignal> runningInflations,
             ApplyCallback applyCallback,
-            NotificationContentInflaterLogger logger) {
+            NotificationRowContentBinderLogger logger) {
         RemoteViews newContentView = applyCallback.getRemoteView();
         if (inflateSynchronously) {
             try {
@@ -845,7 +845,7 @@
     private static void handleInflationError(
             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
             NotificationEntry notification, @Nullable InflationCallback callback,
-            NotificationContentInflaterLogger logger, String logContext) {
+            NotificationRowContentBinderLogger logger, String logContext) {
         Assert.isMainThread();
         logger.logAsyncTaskException(notification, logContext, e);
         runningInflations.values().forEach(CancellationSignal::cancel);
@@ -864,7 +864,7 @@
             @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache,
             HashMap<Integer, CancellationSignal> runningInflations,
             @Nullable InflationCallback endListener, NotificationEntry entry,
-            ExpandableNotificationRow row, NotificationContentInflaterLogger logger) {
+            ExpandableNotificationRow row, NotificationRowContentBinderLogger logger) {
         Assert.isMainThread();
         if (!runningInflations.isEmpty()) {
             return false;
@@ -1080,7 +1080,7 @@
         private final SmartReplyStateInflater mSmartRepliesInflater;
         private final NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider;
         private final HeadsUpStyleProvider mHeadsUpStyleProvider;
-        private final NotificationContentInflaterLogger mLogger;
+        private final NotificationRowContentBinderLogger mLogger;
 
         private AsyncInflationTask(
                 Executor inflationExecutor,
@@ -1099,7 +1099,7 @@
                 SmartReplyStateInflater smartRepliesInflater,
                 NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider,
                 HeadsUpStyleProvider headsUpStyleProvider,
-                NotificationContentInflaterLogger logger) {
+                NotificationRowContentBinderLogger logger) {
             mEntry = entry;
             mRow = row;
             mInflationExecutor = inflationExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index 33339a7..c1302a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -20,6 +20,8 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
 import java.lang.annotation.Retention;
@@ -72,6 +74,10 @@
             @NonNull ExpandableNotificationRow row,
             @InflationFlag int contentToUnbind);
 
+    /** For testing, ensure all inflation is synchronous. */
+    @VisibleForTesting
+    void setInflateSynchronously(boolean inflateSynchronously);
+
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true,
             prefix = {"FLAG_CONTENT_VIEW_"},
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
index 15c7055..a32e1d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderLogger.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
 import javax.inject.Inject
 
-class NotificationContentInflaterLogger
+class NotificationRowContentBinderLogger
 @Inject
 constructor(@NotifInflationLog private val buffer: LogBuffer) {
     fun logNotBindingRowWasRemoved(entry: NotificationEntry) {
@@ -158,4 +158,4 @@
     }
 }
 
-private const val TAG = "NotificationContentInflater"
+private const val TAG = "NotificationRowContentBinder"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index bae89fb..427fb66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -35,7 +35,7 @@
     /**
      * Content views that are out of date and need to be rebound.
      *
-     * TODO: This should go away once {@link NotificationContentInflater} is broken down into
+     * TODO: This should go away once {@link NotificationRowContentBinder} is broken down into
      * smaller stages as then the stage itself would be invalidated.
      */
     private @InflationFlag int mDirtyContentViews = mContentViews;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index 3fce9ce..6fc82c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -342,7 +342,7 @@
         reinflateFlags: Int,
         entry: NotificationEntry,
         context: Context,
-        logger: NotificationContentInflaterLogger,
+        logger: NotificationRowContentBinderLogger,
     ): HybridNotificationView? {
         if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null
         if (reinflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE == 0) {
@@ -354,7 +354,7 @@
 
         var view: HybridNotificationView? = null
 
-        traceSection("NotificationContentInflater#inflateSingleLineView") {
+        traceSection("SingleLineViewInflater#inflateSingleLineView") {
             val inflater = LayoutInflater.from(context)
             val layoutRes: Int =
                 if (isConversation)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index bd7f766..d1fabb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -191,8 +191,12 @@
         updateTransformedTypes();
         addRemainingTransformTypes();
         updateCropToPaddingForImageViews();
-        Notification notification = row.getEntry().getSbn().getNotification();
-        mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon());
+        Notification n = row.getEntry().getSbn().getNotification();
+        if (n.shouldUseAppIcon()) {
+            mIcon.setTag(ImageTransformState.ICON_TAG, n.getAppIcon());
+        } else {
+            mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon());
+        }
 
         // We need to reset all views that are no longer transforming in case a view was previously
         // transformed, but now we decided to transform its container instead.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
index d6c73a9..2dccea6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationHeadsUpCycling.kt
@@ -16,24 +16,22 @@
 
 package com.android.systemui.statusbar.notification.shared
 
-import com.android.systemui.Flags
 import com.android.systemui.flags.FlagToken
-import com.android.systemui.flags.RefactorFlagUtils
 
 /** Helper for reading or using the heads-up cycling flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NotificationHeadsUpCycling {
-    /** The aconfig flag name - enable this feature when FLAG_NOTIFICATION_THROTTLE_HUN is on. */
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN
+    /** The aconfig flag name */
+    const val FLAG_NAME = NotificationThrottleHun.FLAG_NAME
 
     /** A token used for dependency declaration */
     val token: FlagToken
-        get() = FlagToken(FLAG_NAME, isEnabled)
+        get() = NotificationThrottleHun.token
 
     /** Is the heads-up cycling animation enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationThrottleHun()
+        get() = NotificationThrottleHun.isEnabled
 
     /** Whether to animate the bottom line when transiting from a tall HUN to a short HUN */
     @JvmStatic
@@ -46,13 +44,12 @@
      * build to ensure that the refactor author catches issues in testing.
      */
     @JvmStatic
-    inline fun isUnexpectedlyInLegacyMode() =
-        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+    inline fun isUnexpectedlyInLegacyMode() = NotificationThrottleHun.isUnexpectedlyInLegacyMode()
 
     /**
      * Called to ensure code is only run when the flag is disabled. This will throw an exception if
      * the flag is enabled to ensure that the refactor author catches issues in testing.
      */
     @JvmStatic
-    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+    inline fun assertInLegacyMode() = NotificationThrottleHun.assertInLegacyMode()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt
index dd81d42..71f0de0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt
@@ -24,7 +24,7 @@
 @Suppress("NOTHING_TO_INLINE")
 object NotificationThrottleHun {
     /** The aconfig flag name */
-    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_THROTTLE_HUN
 
     /** A token used for dependency declaration */
     val token: FlagToken
@@ -33,7 +33,7 @@
     /** Is the refactor enabled */
     @JvmStatic
     inline val isEnabled
-        get() = Flags.notificationThrottleHun()
+        get() = Flags.notificationAvalancheThrottleHun()
 
     /**
      * Called to ensure code is only run when the flag is enabled. This protects users from the
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
index db544ce..f6722a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
@@ -38,9 +38,6 @@
      */
     val shadeScrimBounds = MutableStateFlow<ShadeScrimBounds?>(null)
 
-    /** the y position of the top of the HUN area */
-    val headsUpTop = MutableStateFlow(0f)
-
     /** height made available to the notifications in the size-constrained mode of lock screen. */
     val constrainedAvailableSpace = MutableStateFlow(0)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index afcf3ae..8557afc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -79,9 +79,6 @@
      */
     val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow()
 
-    /** The y-coordinate in px of bottom of the contents of the HUN. */
-    val headsUpTop: StateFlow<Float> = placeholderRepository.headsUpTop.asStateFlow()
-
     /**
      * The amount in px that the notification stack should scroll due to internal expansion. This
      * should only happen when a notification expansion hits the bottom of the screen, so it is
@@ -125,8 +122,4 @@
     fun setConstrainedAvailableSpace(height: Int) {
         placeholderRepository.constrainedAvailableSpace.value = height
     }
-
-    fun setHeadsUpTop(headsUpTop: Float) {
-        placeholderRepository.headsUpTop.value = headsUpTop
-    }
 }
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 a99fbfc..e90a64a 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
@@ -139,8 +139,6 @@
      */
     val scrolledToTop: Flow<Boolean> =
         stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop")
-    /** The y-coordinate in px of bottom of the contents of the HUN. */
-    val headsUpTop: Flow<Float> = stackAppearanceInteractor.headsUpTop.dumpValue("headsUpTop")
 
     /** Receives the amount (px) that the stack should scroll due to internal expansion. */
     val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index ea33be0..634bd7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -60,10 +60,6 @@
         interactor.setConstrainedAvailableSpace(height)
     }
 
-    fun onHeadsUpTopChanged(headsUpTop: Float) {
-        interactor.setHeadsUpTop(headsUpTop)
-    }
-
     /** Sets the content alpha for the current state of the brightness mirror */
     fun setAlphaForBrightnessMirror(alpha: Float) {
         interactor.setAlphaForBrightnessMirror(alpha)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index b448d85..fc29eab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -413,6 +413,7 @@
         afterKeyguardGone: Boolean,
         customMessage: String?,
     ) {
+        Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone")
         if (
             !action.willRunAnimationOnKeyguard() &&
                 wakefulnessLifecycle.wakefulness == WakefulnessLifecycle.WAKEFULNESS_ASLEEP &&
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index ebb62ec..db4f0af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -112,6 +112,7 @@
 import kotlinx.coroutines.Job;
 
 import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Objects;
@@ -899,6 +900,11 @@
             } finally {
                 Trace.endSection();
             }
+        } else {
+            Log.w(TAG, "Ignoring request to dismiss, dumping state: ");
+            StringWriter sw = new StringWriter();
+            mKeyguardStateController.dump(new PrintWriter(sw), null);
+            Log.w(TAG, sw.toString());
         }
         updateStates();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index c32f0e8..261258a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -40,6 +40,7 @@
 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.dump.DumpManager
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.logDiffsForTable
@@ -99,6 +100,7 @@
     private val context: Context,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     airplaneModeRepository: AirplaneModeRepository,
     // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
     // repository is an input to the mobile repository.
@@ -315,6 +317,7 @@
                 trySend(false)
                 awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
             }
+            .flowOn(mainDispatcher)
             .logDiffsForTable(
                 tableLogger,
                 LOGGING_PREFIX,
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 4325897..1449e53 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
@@ -23,6 +23,7 @@
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +36,7 @@
 import com.android.systemui.statusbar.pipeline.dagger.DeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.dagger.VerboseDeviceBasedSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -162,60 +164,6 @@
     @get:VisibleForTesting
     val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
 
-    init {
-        satelliteManager = satelliteManagerOpt.getOrNull()
-
-        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
-        if (satelliteManager != null) {
-            // First, check that satellite is supported on this device
-            scope.launch {
-                val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
-                if (waitTime > 0) {
-                    logBuffer.i({ long1 = waitTime }) {
-                        "Waiting $long1 ms before checking for satellite support"
-                    }
-                    delay(waitTime)
-                }
-
-                satelliteSupport.value = satelliteManager.checkSatelliteSupported()
-
-                logBuffer.i(
-                    { str1 = satelliteSupport.value.toString() },
-                    { "Checked for system support. support=$str1" },
-                )
-
-                // We only need to check location availability if this mode is supported
-                if (satelliteSupport.value is Supported) {
-                    isSatelliteAllowedForCurrentLocation.subscriptionCount
-                        .map { it > 0 }
-                        .distinctUntilChanged()
-                        .collectLatest { hasSubscribers ->
-                            if (hasSubscribers) {
-                                /*
-                                 * As there is no listener available for checking satellite allowed,
-                                 * we must poll. Defaulting to polling at most once every hour while
-                                 * active. Subsequent OOS events will restart the job, so a flaky
-                                 * connection might cause more frequent checks.
-                                 */
-                                while (true) {
-                                    logBuffer.i {
-                                        "requestIsCommunicationAllowedForCurrentLocation"
-                                    }
-                                    checkIsSatelliteAllowed()
-                                    delay(POLLING_INTERVAL_MS)
-                                }
-                            }
-                        }
-                }
-            }
-        } else {
-            logBuffer.i { "Satellite manager is null" }
-
-            satelliteSupport.value = NotSupported
-        }
-    }
-
     /**
      * 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
@@ -269,6 +217,134 @@
             }
             .onStart { emit(Unit) }
 
+    init {
+        satelliteManager = satelliteManagerOpt.getOrNull()
+
+        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+        if (satelliteManager != null) {
+            // Outer scope launch allows us to delay until MIN_UPTIME
+            scope.launch {
+                // First, check that satellite is supported on this device
+                satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager)
+                logBuffer.i(
+                    { str1 = satelliteSupport.value.toString() },
+                    { "Checked for system support. support=$str1" },
+                )
+
+                // Second, launch a job to poll for service availability based on location
+                scope.launch { pollForAvailabilityBasedOnLocation() }
+
+                // Third, register a listener to let us know if there are changes to support
+                scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
+            }
+        } else {
+            logBuffer.i { "Satellite manager is null" }
+            satelliteSupport.value = NotSupported
+        }
+    }
+
+    private suspend fun checkSatelliteSupportAfterMinUptime(
+        sm: SatelliteManager
+    ): SatelliteSupport {
+        val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
+        if (waitTime > 0) {
+            logBuffer.i({ long1 = waitTime }) {
+                "Waiting $long1 ms before checking for satellite support"
+            }
+            delay(waitTime)
+        }
+
+        return sm.checkSatelliteSupported()
+    }
+
+    /*
+     * As there is no listener available for checking satellite allowed, we must poll the service.
+     * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
+     * the job, so a flaky connection might cause more frequent checks.
+     */
+    private suspend fun pollForAvailabilityBasedOnLocation() {
+        satelliteSupport
+            .whenSupported(
+                supported = ::isSatelliteAllowedHasListener,
+                orElse = flowOf(false),
+                retrySignal = telephonyProcessCrashedEvent,
+            )
+            .collectLatest { hasSubscribers ->
+                if (hasSubscribers) {
+                    while (true) {
+                        logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
+                        checkIsSatelliteAllowed()
+                        delay(POLLING_INTERVAL_MS)
+                    }
+                }
+            }
+    }
+
+    /**
+     * Register a callback with [SatelliteManager] to let us know if there is a change in satellite
+     * support. This job restarts if there is a crash event detected.
+     *
+     * Note that the structure of this method looks similar to [whenSupported], but since we want
+     * this callback registered even when it is [NotSupported], we just mimic the structure here.
+     */
+    private suspend fun listenForChangesToSatelliteSupport(sm: SatelliteManager) {
+        telephonyProcessCrashedEvent.collectLatest {
+            satelliteIsSupportedCallback.collect { supported ->
+                if (supported) {
+                    satelliteSupport.value = Supported(sm)
+                } else {
+                    satelliteSupport.value = NotSupported
+                }
+            }
+        }
+    }
+
+    /**
+     * Callback version of [checkSatelliteSupported]. This flow should be retried on the same
+     * [telephonyProcessCrashedEvent] signal, but does not require a [SupportedSatelliteManager],
+     * since it specifically watches for satellite support.
+     */
+    private val satelliteIsSupportedCallback: Flow<Boolean> =
+        if (satelliteManager == null) {
+            flowOf(false)
+        } else {
+            conflatedCallbackFlow {
+                val callback = SatelliteSupportedStateCallback { supported ->
+                    logBuffer.i {
+                        "onSatelliteSupportedStateChanged: " +
+                            "${if (supported) "supported" else "not supported"}"
+                    }
+                    trySend(supported)
+                }
+
+                var registered = false
+                try {
+                    satelliteManager.registerForSupportedStateChanged(
+                        bgDispatcher.asExecutor(),
+                        callback
+                    )
+                    registered = true
+                } catch (e: Exception) {
+                    logBuffer.e("error registering for supported state change", e)
+                }
+
+                awaitClose {
+                    if (registered) {
+                        satelliteManager.unregisterForSupportedStateChanged(callback)
+                    }
+                }
+            }
+        }
+
+    /**
+     * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
+     * are active listeners to [isSatelliteAllowedForCurrentLocation]
+     */
+    @SuppressWarnings("unused")
+    private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
+        isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
+
     override val connectionState =
         satelliteSupport
             .whenSupported(
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 eb09e6e..a972985 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -17,6 +17,8 @@
 
 import android.util.Log
 import androidx.annotation.VisibleForTesting
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
@@ -35,6 +37,7 @@
 @Inject
 constructor(
     dumpManager: DumpManager,
+    private val uiEventLogger: UiEventLogger
 ) : Dumpable {
 
     private val tag = "AvalancheController"
@@ -65,6 +68,21 @@
     // For debugging only
     @VisibleForTesting var debugDropSet: MutableSet<HeadsUpEntry> = HashSet()
 
+    enum class ThrottleEvent(private val id: Int) : UiEventLogger.UiEventEnum {
+        @UiEvent(doc = "HUN was shown.")
+        SHOWN(1812),
+
+        @UiEvent(doc = "HUN was dropped to show higher priority HUNs.")
+        DROPPED(1813),
+
+        @UiEvent(doc = "HUN was removed while waiting to show.")
+        REMOVED(1814);
+
+        override fun getId(): Int {
+            return id
+        }
+    }
+
     init {
         dumpManager.registerNormalDumpable(tag, /* module */ this)
     }
@@ -145,6 +163,7 @@
             log { "$fn => remove from next" }
             if (entry in nextMap) nextMap.remove(entry)
             if (entry in nextList) nextList.remove(entry)
+            uiEventLogger.log(ThrottleEvent.REMOVED)
         } else if (entry in debugDropSet) {
             log { "$fn => remove from dropset" }
             debugDropSet.remove(entry)
@@ -268,6 +287,7 @@
     private fun showNow(entry: HeadsUpEntry, runnableList: MutableList<Runnable>) {
         log { "SHOW: " + getKey(entry) }
 
+        uiEventLogger.log(ThrottleEvent.SHOWN)
         headsUpEntryShowing = entry
 
         runnableList.forEach {
@@ -295,6 +315,12 @@
 
         // Remove runnable labels for dropped huns
         val listToDrop = nextList.subList(1, nextList.size)
+
+        // Log dropped HUNs
+        for (e in listToDrop) {
+            uiEventLogger.log(ThrottleEvent.DROPPED)
+        }
+
         if (debug) {
             // Clear runnable labels
             for (e in listToDrop) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
index b07aa81..d210e93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateController.java
@@ -18,13 +18,16 @@
 
 import android.app.IActivityTaskManager;
 
+import com.android.systemui.Dumpable;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.KeyguardStateController.Callback;
 
+import java.io.PrintWriter;
+
 /**
  * Source of truth for keyguard state: If locked, occluded, has password, trusted etc.
  */
-public interface KeyguardStateController extends CallbackController<Callback> {
+public interface KeyguardStateController extends CallbackController<Callback>, Dumpable {
 
     /**
      * If the device is locked or unlocked.
@@ -40,6 +43,8 @@
         return isShowing() && !isOccluded();
     }
 
+    default void dump(PrintWriter pw, String[] args) { }
+
     /**
      * If the keyguard is showing. This includes when it's occluded by an activity, and when
      * the device is asleep or in always on mode, except when the screen timed out and the user
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
index 886010c..c256e64 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardStateControllerImpl.java
@@ -36,7 +36,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
-import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -57,7 +56,7 @@
  *
  */
 @SysUISingleton
-public class KeyguardStateControllerImpl implements KeyguardStateController, Dumpable {
+public class KeyguardStateControllerImpl implements KeyguardStateController {
 
     private static final boolean DEBUG_AUTH_WITH_ADB = false;
     private static final String AUTH_BROADCAST_KEY = "debug_trigger_auth";
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
deleted file mode 100644
index 2cad844..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2019 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.util.concurrency;
-
-import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Process;
-
-import com.android.systemui.Flags;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.BroadcastRunning;
-import com.android.systemui.dagger.qualifiers.LongRunning;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dagger.qualifiers.NotifInflation;
-
-import dagger.Module;
-import dagger.Provides;
-
-import java.util.concurrent.Executor;
-
-import javax.inject.Named;
-
-/**
- * Dagger Module for classes found within the concurrent package.
- */
-@Module
-public abstract class SysUIConcurrencyModule {
-
-    // Slow BG executor can potentially affect UI if UI is waiting for an updated state from this
-    // thread
-    private static final Long BG_SLOW_DISPATCH_THRESHOLD = 1000L;
-    private static final Long BG_SLOW_DELIVERY_THRESHOLD = 1000L;
-    private static final Long LONG_SLOW_DISPATCH_THRESHOLD = 2500L;
-    private static final Long LONG_SLOW_DELIVERY_THRESHOLD = 2500L;
-    private static final Long BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L;
-    private static final Long BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L;
-    private static final Long NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD = 1000L;
-    private static final Long NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD = 1000L;
-
-    /** Background Looper */
-    @Provides
-    @SysUISingleton
-    @Background
-    public static Looper provideBgLooper() {
-        HandlerThread thread = new HandlerThread("SysUiBg",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        thread.getLooper().setSlowLogThresholdMs(BG_SLOW_DISPATCH_THRESHOLD,
-                BG_SLOW_DELIVERY_THRESHOLD);
-        return thread.getLooper();
-    }
-
-    /** BroadcastRunning Looper (for sending and receiving broadcasts) */
-    @Provides
-    @SysUISingleton
-    @BroadcastRunning
-    public static Looper provideBroadcastRunningLooper() {
-        HandlerThread thread = new HandlerThread("BroadcastRunning",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        thread.getLooper().setSlowLogThresholdMs(BROADCAST_SLOW_DISPATCH_THRESHOLD,
-                BROADCAST_SLOW_DELIVERY_THRESHOLD);
-        return thread.getLooper();
-    }
-
-    /** Long running tasks Looper */
-    @Provides
-    @SysUISingleton
-    @LongRunning
-    public static Looper provideLongRunningLooper() {
-        HandlerThread thread = new HandlerThread("SysUiLng",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        thread.getLooper().setSlowLogThresholdMs(LONG_SLOW_DISPATCH_THRESHOLD,
-                LONG_SLOW_DELIVERY_THRESHOLD);
-        return thread.getLooper();
-    }
-
-    /** Notification inflation Looper */
-    @Provides
-    @SysUISingleton
-    @NotifInflation
-    public static Looper provideNotifInflationLooper(@Background Looper bgLooper) {
-        if (!Flags.dedicatedNotifInflationThread()) {
-            return bgLooper;
-        }
-
-        final HandlerThread thread = new HandlerThread("NotifInflation",
-                Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        final Looper looper = thread.getLooper();
-        looper.setSlowLogThresholdMs(NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD,
-                NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD);
-        return looper;
-    }
-
-    /**
-     * Background Handler.
-     *
-     * Prefer the Background Executor when possible.
-     */
-    @Provides
-    @Background
-    public static Handler provideBgHandler(@Background Looper bgLooper) {
-        return new Handler(bgLooper);
-    }
-
-    /**
-     * Provide a BroadcastRunning Executor (for sending and receiving broadcasts).
-     */
-    @Provides
-    @SysUISingleton
-    @BroadcastRunning
-    public static Executor provideBroadcastRunningExecutor(@BroadcastRunning Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
-     * Provide a Long running Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @LongRunning
-    public static Executor provideLongRunningExecutor(@LongRunning Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
-     * Provide a Long running Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @LongRunning
-    public static DelayableExecutor provideLongRunningDelayableExecutor(
-            @LongRunning Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
-     * Provide a Background-Thread Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @Background
-    public static Executor provideBackgroundExecutor(@Background Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
-     * Provide a Background-Thread Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @Background
-    public static DelayableExecutor provideBackgroundDelayableExecutor(@Background Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-
-    /**
-     * Provide a Background-Thread Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @Background
-    public static RepeatableExecutor provideBackgroundRepeatableExecutor(
-            @Background DelayableExecutor exec) {
-        return new RepeatableExecutorImpl(exec);
-    }
-
-    /**
-     * Provide a Main-Thread Executor.
-     */
-    @Provides
-    @SysUISingleton
-    @Main
-    public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) {
-        return new RepeatableExecutorImpl(exec);
-    }
-
-    /** */
-    @Provides
-    @Main
-    public static MessageRouter providesMainMessageRouter(
-            @Main DelayableExecutor executor) {
-        return new MessageRouterImpl(executor);
-    }
-
-    /** */
-    @Provides
-    @Background
-    public static MessageRouter providesBackgroundMessageRouter(
-            @Background DelayableExecutor executor) {
-        return new MessageRouterImpl(executor);
-    }
-
-    /** */
-    @Provides
-    @SysUISingleton
-    @Named(TIME_TICK_HANDLER_NAME)
-    public static Handler provideTimeTickHandler() {
-        HandlerThread thread = new HandlerThread("TimeTick");
-        thread.start();
-        return new Handler(thread.getLooper());
-    }
-
-    /** */
-    @Provides
-    @SysUISingleton
-    @NotifInflation
-    public static Executor provideNotifInflationExecutor(@NotifInflation Looper looper) {
-        return new ExecutorImpl(looper);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
new file mode 100644
index 0000000..a7abb6b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
@@ -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.systemui.util.concurrency
+
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Process
+import android.view.Choreographer
+import com.android.systemui.Dependency
+import com.android.systemui.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.BroadcastRunning
+import com.android.systemui.dagger.qualifiers.LongRunning
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dagger.qualifiers.NotifInflation
+import dagger.Module
+import dagger.Provides
+import java.util.concurrent.Executor
+import javax.inject.Named
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class BackPanelUiThread
+
+/** Dagger Module for classes found within the concurrent package. */
+@Module
+object SysUIConcurrencyModule {
+    // Slow BG executor can potentially affect UI if UI is waiting for an updated state from this
+    // thread
+    private const val BG_SLOW_DISPATCH_THRESHOLD = 1000L
+    private const val BG_SLOW_DELIVERY_THRESHOLD = 1000L
+    private const val LONG_SLOW_DISPATCH_THRESHOLD = 2500L
+    private const val LONG_SLOW_DELIVERY_THRESHOLD = 2500L
+    private const val BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L
+    private const val BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L
+    private const val NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD = 1000L
+    private const val NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD = 1000L
+
+    /** Background Looper */
+    @Provides
+    @SysUISingleton
+    @Background
+    fun provideBgLooper(): Looper {
+        val thread = HandlerThread("SysUiBg", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        thread
+            .getLooper()
+            .setSlowLogThresholdMs(BG_SLOW_DISPATCH_THRESHOLD, BG_SLOW_DELIVERY_THRESHOLD)
+        return thread.getLooper()
+    }
+
+    /** BroadcastRunning Looper (for sending and receiving broadcasts) */
+    @Provides
+    @SysUISingleton
+    @BroadcastRunning
+    fun provideBroadcastRunningLooper(): Looper {
+        val thread = HandlerThread("BroadcastRunning", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        thread
+            .getLooper()
+            .setSlowLogThresholdMs(
+                BROADCAST_SLOW_DISPATCH_THRESHOLD,
+                BROADCAST_SLOW_DELIVERY_THRESHOLD
+            )
+        return thread.getLooper()
+    }
+
+    /** Long running tasks Looper */
+    @Provides
+    @SysUISingleton
+    @LongRunning
+    fun provideLongRunningLooper(): Looper {
+        val thread = HandlerThread("SysUiLng", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        thread
+            .getLooper()
+            .setSlowLogThresholdMs(LONG_SLOW_DISPATCH_THRESHOLD, LONG_SLOW_DELIVERY_THRESHOLD)
+        return thread.getLooper()
+    }
+
+    /** Notification inflation Looper */
+    @Provides
+    @SysUISingleton
+    @NotifInflation
+    fun provideNotifInflationLooper(@Background bgLooper: Looper): Looper {
+        if (!Flags.dedicatedNotifInflationThread()) {
+            return bgLooper
+        }
+        val thread = HandlerThread("NotifInflation", Process.THREAD_PRIORITY_BACKGROUND)
+        thread.start()
+        val looper = thread.getLooper()
+        looper.setSlowLogThresholdMs(
+            NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD,
+            NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD
+        )
+        return looper
+    }
+
+    @Provides
+    @SysUISingleton
+    @BackPanelUiThread
+    fun provideBackPanelUiThreadContext(
+        @Main mainLooper: Looper,
+        @Main mainHandler: Handler,
+        @Main mainExecutor: Executor
+    ): UiThreadContext {
+        return if (Flags.edgeBackGestureHandlerThread()) {
+            val thread =
+                HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply {
+                    start()
+                    looper.setSlowLogThresholdMs(
+                        LONG_SLOW_DISPATCH_THRESHOLD,
+                        LONG_SLOW_DELIVERY_THRESHOLD
+                    )
+                }
+            UiThreadContext(
+                thread.looper,
+                thread.threadHandler,
+                thread.threadExecutor,
+                thread.threadHandler.runWithScissors { Choreographer.getInstance() }
+            )
+        } else {
+            UiThreadContext(
+                mainLooper,
+                mainHandler,
+                mainExecutor,
+                mainHandler.runWithScissors { Choreographer.getInstance() }
+            )
+        }
+    }
+
+    /**
+     * Background Handler.
+     *
+     * Prefer the Background Executor when possible.
+     */
+    @Provides
+    @Background
+    fun provideBgHandler(@Background bgLooper: Looper): Handler = Handler(bgLooper)
+
+    /** Provide a BroadcastRunning Executor (for sending and receiving broadcasts). */
+    @Provides
+    @SysUISingleton
+    @BroadcastRunning
+    fun provideBroadcastRunningExecutor(@BroadcastRunning looper: Looper): Executor =
+        ExecutorImpl(looper)
+
+    /** Provide a Long running Executor. */
+    @Provides
+    @SysUISingleton
+    @LongRunning
+    fun provideLongRunningExecutor(@LongRunning looper: Looper): Executor = ExecutorImpl(looper)
+
+    /** Provide a Long running Executor. */
+    @Provides
+    @SysUISingleton
+    @LongRunning
+    fun provideLongRunningDelayableExecutor(@LongRunning looper: Looper): DelayableExecutor =
+        ExecutorImpl(looper)
+
+    /** Provide a Background-Thread Executor. */
+    @Provides
+    @SysUISingleton
+    @Background
+    fun provideBackgroundExecutor(@Background looper: Looper): Executor = ExecutorImpl(looper)
+
+    /** Provide a Background-Thread Executor. */
+    @Provides
+    @SysUISingleton
+    @Background
+    fun provideBackgroundDelayableExecutor(@Background looper: Looper): DelayableExecutor =
+        ExecutorImpl(looper)
+
+    /** Provide a Background-Thread Executor. */
+    @Provides
+    @SysUISingleton
+    @Background
+    fun provideBackgroundRepeatableExecutor(
+        @Background exec: DelayableExecutor
+    ): RepeatableExecutor = RepeatableExecutorImpl(exec)
+
+    /** Provide a Main-Thread Executor. */
+    @Provides
+    @SysUISingleton
+    @Main
+    fun provideMainRepeatableExecutor(@Main exec: DelayableExecutor): RepeatableExecutor =
+        RepeatableExecutorImpl(exec)
+
+    /**  */
+    @Provides
+    @Main
+    fun providesMainMessageRouter(@Main executor: DelayableExecutor): MessageRouter =
+        MessageRouterImpl(executor)
+
+    /**  */
+    @Provides
+    @Background
+    fun providesBackgroundMessageRouter(@Background executor: DelayableExecutor): MessageRouter =
+        MessageRouterImpl(executor)
+
+    /**  */
+    @Provides
+    @SysUISingleton
+    @Named(Dependency.TIME_TICK_HANDLER_NAME)
+    fun provideTimeTickHandler(): Handler {
+        val thread = HandlerThread("TimeTick")
+        thread.start()
+        return Handler(thread.getLooper())
+    }
+
+    /**  */
+    @Provides
+    @SysUISingleton
+    @NotifInflation
+    fun provideNotifInflationExecutor(@NotifInflation looper: Looper): Executor =
+        ExecutorImpl(looper)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt
new file mode 100644
index 0000000..8c8c686
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/UiThreadContext.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.util.concurrency
+
+import android.os.Handler
+import android.os.Looper
+import android.view.Choreographer
+import com.android.systemui.util.Assert
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+private const val DEFAULT_TIMEOUT = 150L
+
+class UiThreadContext(
+    val looper: Looper,
+    val handler: Handler,
+    val executor: Executor,
+    val choreographer: Choreographer
+) {
+    fun isCurrentThread() {
+        Assert.isCurrentThread(looper)
+    }
+
+    fun <T> runWithScissors(block: () -> T): T {
+        return handler.runWithScissors(block)
+    }
+
+    fun runWithScissors(block: Runnable) {
+        handler.runWithScissors(block, DEFAULT_TIMEOUT)
+    }
+}
+
+fun <T> Handler.runWithScissors(block: () -> T): T {
+    val returnedValue = AtomicReference<T>()
+    runWithScissors({ returnedValue.set(block()) }, DEFAULT_TIMEOUT)
+    return returnedValue.get()!!
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
index c6b0dc5..154737c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt
@@ -17,17 +17,18 @@
 package com.android.systemui.volume.domain.interactor
 
 import android.bluetooth.BluetoothAdapter
+import android.content.Context
 import android.media.AudioDeviceInfo
-import android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES
-import android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.LocalBluetoothManager
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.MediaDevice.MediaDeviceType
+import com.android.settingslib.media.PhoneMediaDevice
 import com.android.settingslib.volume.data.repository.AudioRepository
 import com.android.settingslib.volume.data.repository.AudioSharingRepository
 import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.volume.domain.model.AudioOutputDevice
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
@@ -38,6 +39,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
@@ -49,6 +51,7 @@
 class AudioOutputInteractor
 @Inject
 constructor(
+    @Application private val context: Context,
     audioRepository: AudioRepository,
     audioModeInteractor: AudioModeInteractor,
     @VolumePanelScope scope: CoroutineScope,
@@ -60,7 +63,7 @@
     audioSharingRepository: AudioSharingRepository,
 ) {
 
-    val currentAudioDevice: Flow<AudioOutputDevice> =
+    val currentAudioDevice: StateFlow<AudioOutputDevice> =
         audioModeInteractor.isOngoingCall
             .flatMapLatest { isOngoingCall ->
                 if (isOngoingCall) {
@@ -81,30 +84,32 @@
     val isInAudioSharing: Flow<Boolean> = audioSharingRepository.inAudioSharing
 
     private fun AudioDeviceInfo.toAudioOutputDevice(): AudioOutputDevice {
-        if (type == TYPE_WIRED_HEADPHONES || type == TYPE_WIRED_HEADSET) {
+        if (
+            BluetoothAdapter.checkBluetoothAddress(address) &&
+                localBluetoothManager != null &&
+                bluetoothAdapter != null
+        ) {
+            val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
+            localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)?.let {
+                device: CachedBluetoothDevice ->
+                return AudioOutputDevice.Bluetooth(
+                    name = device.name,
+                    icon = deviceIconInteractor.loadIcon(device),
+                    cachedBluetoothDevice = device,
+                )
+            }
+        }
+        // Built-in device has an empty address
+        if (address.isNotEmpty()) {
             return AudioOutputDevice.Wired(
                 name = productName.toString(),
                 icon = deviceIconInteractor.loadIcon(type),
             )
         }
-        val cachedBluetoothDevice: CachedBluetoothDevice? =
-            if (address.isEmpty() || localBluetoothManager == null || bluetoothAdapter == null) {
-                null
-            } else {
-                val remoteDevice = bluetoothAdapter.getRemoteDevice(address)
-                localBluetoothManager.cachedDeviceManager.findDevice(remoteDevice)
-            }
-        return cachedBluetoothDevice?.let {
-            AudioOutputDevice.Bluetooth(
-                name = it.name,
-                icon = deviceIconInteractor.loadIcon(it),
-                cachedBluetoothDevice = it,
-            )
-        }
-            ?: AudioOutputDevice.BuiltIn(
-                name = productName.toString(),
-                icon = deviceIconInteractor.loadIcon(type),
-            )
+        return AudioOutputDevice.BuiltIn(
+            name = PhoneMediaDevice.getMediaTransferThisDeviceName(context),
+            icon = deviceIconInteractor.loadIcon(type),
+        )
     }
 
     private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice {
@@ -115,7 +120,8 @@
                     icon = icon,
                     cachedBluetoothDevice = cachedDevice,
                 )
-            deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE ->
+            deviceType == MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE ||
+                deviceType == MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE ->
                 AudioOutputDevice.Wired(
                     name = name,
                     icon = icon,
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 0dc2647..79a4ae7 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
@@ -19,14 +19,13 @@
 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
 import kotlinx.coroutines.CoroutineScope
 
 interface LocalMediaRepositoryFactory {
 
-    fun create(packageName: String?): LocalMediaRepository
+    fun create(packageName: String?, coroutineScope: CoroutineScope): LocalMediaRepository
 }
 
 @SysUISingleton
@@ -35,10 +34,12 @@
 constructor(
     private val eventsReceiver: AudioManagerEventsReceiver,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
-    @Application private val coroutineScope: CoroutineScope,
 ) : LocalMediaRepositoryFactory {
 
-    override fun create(packageName: String?): LocalMediaRepository =
+    override fun create(
+        packageName: String?,
+        coroutineScope: CoroutineScope
+    ): LocalMediaRepository =
         LocalMediaRepositoryImpl(
             eventsReceiver,
             localMediaManagerFactory.create(packageName),
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
index 199bc3b..333f4ad 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.animation.Expandable
 import com.android.systemui.media.dialog.MediaOutputDialogManager
-import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlaybackState
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaOutputComponentModel
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import javax.inject.Inject
 
@@ -29,14 +29,12 @@
 @VolumePanelScope
 class MediaOutputActionsInteractor
 @Inject
-constructor(
-    private val mediaOutputDialogManager: MediaOutputDialogManager,
-) {
+constructor(private val mediaOutputDialogManager: MediaOutputDialogManager) {
 
-    fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable?) {
-        if (sessionWithPlaybackState?.isPlaybackActive == true) {
+    fun onBarClick(model: MediaOutputComponentModel?, expandable: Expandable?) {
+        if (model is MediaOutputComponentModel.MediaSession) {
             mediaOutputDialogManager.createAndShowWithController(
-                sessionWithPlaybackState.session.packageName,
+                model.session.packageName,
                 false,
                 expandable?.dialogController()
             )
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
new file mode 100644
index 0000000..ed25129
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractor.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.component.mediaoutput.domain.interactor
+
+import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
+import com.android.systemui.volume.domain.interactor.AudioOutputInteractor
+import com.android.systemui.volume.domain.model.AudioOutputDevice
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaOutputComponentModel
+import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlaybackState
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.shared.model.Result
+import com.android.systemui.volume.panel.shared.model.filterData
+import com.android.systemui.volume.panel.shared.model.wrapInResult
+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.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
+
+/** Gathers together a domain state for the Media Output Volume Panel component. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaOutputComponentInteractor
+@Inject
+constructor(
+    @VolumePanelScope private val coroutineScope: CoroutineScope,
+    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
+    audioOutputInteractor: AudioOutputInteractor,
+    audioModeInteractor: AudioModeInteractor,
+    interactor: MediaOutputInteractor,
+) {
+
+    private val sessionWithPlaybackState: StateFlow<Result<SessionWithPlaybackState?>> =
+        interactor.defaultActiveMediaSession
+            .filterData()
+            .flatMapLatest { session ->
+                if (session == null) {
+                    flowOf(null)
+                } else {
+                    mediaDeviceSessionInteractor.playbackState(session).mapNotNull { playback ->
+                        playback?.let { SessionWithPlaybackState(session, playback.isActive) }
+                    }
+                }
+            }
+            .wrapInResult()
+            .stateIn(
+                coroutineScope,
+                SharingStarted.Eagerly,
+                Result.Loading(),
+            )
+
+    private val currentAudioDevice: Flow<AudioOutputDevice> =
+        audioOutputInteractor.currentAudioDevice.filter { it !is AudioOutputDevice.Unknown }
+
+    val mediaOutputModel: StateFlow<Result<MediaOutputComponentModel>> =
+        audioModeInteractor.isOngoingCall
+            .flatMapLatest { isOngoingCall ->
+                audioOutputInteractor.isInAudioSharing.flatMapLatest { isInAudioSharing ->
+                    if (isOngoingCall) {
+                        currentAudioDevice.map {
+                            MediaOutputComponentModel.Calling(it, isInAudioSharing)
+                        }
+                    } else {
+                        combine(sessionWithPlaybackState.filterData(), currentAudioDevice) {
+                            sessionWithPlaybackState,
+                            currentAudioDevice ->
+                            if (sessionWithPlaybackState == null) {
+                                MediaOutputComponentModel.Idle(currentAudioDevice, isInAudioSharing)
+                            } else {
+                                MediaOutputComponentModel.MediaSession(
+                                    sessionWithPlaybackState.session,
+                                    sessionWithPlaybackState.isPlaybackActive,
+                                    currentAudioDevice,
+                                    isInAudioSharing,
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+            .wrapInResult()
+            .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
index 9fbd79a..31a8977 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt
@@ -35,6 +35,7 @@
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -46,6 +47,7 @@
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
 import kotlinx.coroutines.withContext
 
 /** Provides observable models about the current media session state. */
@@ -105,12 +107,9 @@
             .filterData()
             .map { it?.packageName }
             .distinctUntilChanged()
-            .map { localMediaRepositoryFactory.create(it) }
-            .stateIn(
-                coroutineScope,
-                SharingStarted.Eagerly,
-                localMediaRepositoryFactory.create(null)
-            )
+            .transformLatest {
+                coroutineScope { emit(localMediaRepositoryFactory.create(it, this)) }
+            }
 
     /** Currently connected [MediaDevice]. */
     val currentConnectedDevice: Flow<MediaDevice?> =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt
new file mode 100644
index 0000000..220fb2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaOutputComponentModel.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.component.mediaoutput.domain.model
+
+import com.android.systemui.volume.domain.model.AudioOutputDevice
+import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession
+
+/** Models domain data for the Media Output Component */
+sealed interface MediaOutputComponentModel {
+
+    val device: AudioOutputDevice
+    val isInAudioSharing: Boolean
+
+    /** There is an ongoing call on the device. */
+    data class Calling(
+        override val device: AudioOutputDevice,
+        override val isInAudioSharing: Boolean,
+    ) : MediaOutputComponentModel
+
+    /** There is media playing on the device. */
+    data class MediaSession(
+        val session: MediaDeviceSession,
+        val isPlaybackActive: Boolean,
+        override val device: AudioOutputDevice,
+        override val isInAudioSharing: Boolean,
+    ) : MediaOutputComponentModel
+
+    /** There is nothing playing on the device. */
+    data class Idle(
+        override val device: AudioOutputDevice,
+        override val isInAudioSharing: Boolean,
+    ) : MediaOutputComponentModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
index 40b7977..36b42f2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt
@@ -18,33 +18,24 @@
 
 import android.content.Context
 import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.volume.domain.interactor.AudioModeInteractor
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Color
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.res.R
-import com.android.systemui.volume.domain.interactor.AudioOutputInteractor
 import com.android.systemui.volume.domain.model.AudioOutputDevice
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
 import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlaybackState
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputComponentInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaOutputComponentModel
 import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
 import com.android.systemui.volume.panel.shared.model.Result
 import com.android.systemui.volume.panel.shared.model.filterData
-import com.android.systemui.volume.panel.shared.model.wrapInResult
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
 
 /** Models the UI of the Media Output Volume Panel component. */
@@ -56,61 +47,40 @@
     private val context: Context,
     @VolumePanelScope private val coroutineScope: CoroutineScope,
     private val actionsInteractor: MediaOutputActionsInteractor,
-    private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
-    private val audioOutputInteractor: AudioOutputInteractor,
-    audioModeInteractor: AudioModeInteractor,
-    interactor: MediaOutputInteractor,
+    private val mediaOutputComponentInteractor: MediaOutputComponentInteractor,
     private val uiEventLogger: UiEventLogger,
 ) {
 
-    private val sessionWithPlaybackState: StateFlow<Result<SessionWithPlaybackState?>> =
-        interactor.defaultActiveMediaSession
-            .filterData()
-            .flatMapLatest { session ->
-                if (session == null) {
-                    flowOf(null)
-                } else {
-                    mediaDeviceSessionInteractor.playbackState(session).mapNotNull { playback ->
-                        playback?.let { SessionWithPlaybackState(session, playback.isActive) }
-                    }
-                }
-            }
-            .wrapInResult()
-            .stateIn(
-                coroutineScope,
-                SharingStarted.Eagerly,
-                Result.Loading(),
-            )
-
     val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> =
-        combine(
-                sessionWithPlaybackState.filterData(),
-                audioModeInteractor.isOngoingCall,
-                audioOutputInteractor.currentAudioDevice.filter {
-                    it !is AudioOutputDevice.Unknown
-                },
-                audioOutputInteractor.isInAudioSharing,
-            ) { mediaDeviceSession, isOngoingCall, currentConnectedDevice, isInAudioSharing ->
+        mediaOutputComponentInteractor.mediaOutputModel
+            .filterData()
+            .map { mediaOutputModel ->
                 val label =
-                    when {
-                        isOngoingCall -> context.getString(R.string.media_output_title_ongoing_call)
-                        mediaDeviceSession?.isPlaybackActive == true ->
-                            context.getString(
-                                R.string.media_output_label_title,
-                                mediaDeviceSession.session.appLabel
-                            )
-                        else -> context.getString(R.string.media_output_title_without_playing)
+                    when (mediaOutputModel) {
+                        is MediaOutputComponentModel.Idle -> {
+                            context.getString(R.string.media_output_title_without_playing)
+                        }
+                        is MediaOutputComponentModel.MediaSession -> {
+                            if (mediaOutputModel.isPlaybackActive) {
+                                context.getString(
+                                    R.string.media_output_label_title,
+                                    mediaOutputModel.session.appLabel,
+                                )
+                            } else {
+                                context.getString(R.string.media_output_title_without_playing)
+                            }
+                        }
+                        is MediaOutputComponentModel.Calling -> {
+                            context.getString(R.string.media_output_title_ongoing_call)
+                        }
                     }
                 ConnectedDeviceViewModel(
                     label,
-                    when (isInAudioSharing) {
-                        true -> {
-                            context.getString(R.string.audio_sharing_description)
-                        }
-                        false -> {
-                            currentConnectedDevice.name
-                        }
-                    }
+                    if (mediaOutputModel.isInAudioSharing) {
+                        context.getString(R.string.audio_sharing_description)
+                    } else {
+                        mediaOutputModel.device.name
+                    },
                 )
             }
             .stateIn(
@@ -120,16 +90,20 @@
             )
 
     val deviceIconViewModel: StateFlow<DeviceIconViewModel?> =
-        combine(sessionWithPlaybackState.filterData(), audioOutputInteractor.currentAudioDevice) {
-                mediaDeviceSession,
-                currentConnectedDevice ->
+        mediaOutputComponentInteractor.mediaOutputModel
+            .filterData()
+            .map { mediaOutputModel ->
                 val icon: Icon =
-                    currentConnectedDevice
-                        .takeIf { currentConnectedDevice !is AudioOutputDevice.Unknown }
+                    mediaOutputModel.device
+                        .takeIf { it !is AudioOutputDevice.Unknown }
                         ?.icon
                         ?.let { Icon.Loaded(it, null) }
                         ?: Icon.Resource(R.drawable.ic_media_home_devices, null)
-                if (mediaDeviceSession?.isPlaybackActive == true) {
+                val isPlaybackActive =
+                    (mediaOutputModel as? MediaOutputComponentModel.MediaSession)
+                        ?.isPlaybackActive == true
+                val isCalling = mediaOutputModel is MediaOutputComponentModel.Calling
+                if (isPlaybackActive || isCalling) {
                     DeviceIconViewModel.IsPlaying(
                         icon = icon,
                         iconColor =
@@ -156,8 +130,9 @@
             )
 
     val enabled: StateFlow<Boolean> =
-        audioOutputInteractor.isInAudioSharing
-            .map { !it }
+        mediaOutputComponentInteractor.mediaOutputModel
+            .filterData()
+            .map { !it.isInAudioSharing }
             .stateIn(
                 coroutineScope,
                 SharingStarted.Eagerly,
@@ -166,7 +141,11 @@
 
     fun onBarClick(expandable: Expandable?) {
         uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED)
-        val result = sessionWithPlaybackState.value
-        actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable)
+        val result: Result<MediaOutputComponentModel> =
+            mediaOutputComponentInteractor.mediaOutputModel.value
+        actionsInteractor.onBarClick(
+            (result as? Result.Data<MediaOutputComponentModel>)?.data,
+            expandable
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
index 4b2d26a..6c6a1cc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
@@ -48,15 +48,31 @@
     private val uiEventLogger: UiEventLogger,
 ) {
 
+    private val spatialSpeakerIcon =
+        Icon.Resource(R.drawable.ic_spatial_speaker, contentDescription = null)
+
     val spatialAudioButton: StateFlow<ButtonViewModel?> =
-        interactor.isEnabled
-            .map {
-                val isChecked = it is SpatialAudioEnabledModel.SpatialAudioEnabled
-                it.toViewModel(isChecked)
+        combine(interactor.isEnabled, interactor.isAvailable) { isEnabled, isAvailable ->
+                isEnabled
+                    .toViewModel(
+                        isChecked = isEnabled is SpatialAudioEnabledModel.SpatialAudioEnabled,
+                        isHeadTrackingAvailable =
+                            isAvailable is SpatialAudioAvailabilityModel.SpatialAudio,
+                    )
                     .copy(label = context.getString(R.string.volume_panel_spatial_audio_title))
             }
             .stateIn(scope, SharingStarted.Eagerly, null)
 
+    val shouldUsePopup: StateFlow<Boolean> =
+        interactor.isAvailable
+            .map {
+                // head tracking availability means there are three possible states for the spatial
+                // audio: disabled, enabled regular, enabled with head tracking.
+                // Show popup in this case instead of a togglealbe button.
+                it is SpatialAudioAvailabilityModel.SpatialAudio
+            }
+            .stateIn(scope, SharingStarted.Eagerly, false)
+
     val isAvailable: StateFlow<Boolean> =
         availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true)
 
@@ -73,8 +89,12 @@
                         }
                     }
                     .map { isEnabled ->
-                        val isChecked = isEnabled == currentIsEnabled
-                        val buttonViewModel: ButtonViewModel = isEnabled.toViewModel(isChecked)
+                        val buttonViewModel: ButtonViewModel =
+                            isEnabled.toViewModel(
+                                isChecked = isEnabled == currentIsEnabled,
+                                isHeadTrackingAvailable =
+                                    isAvailable is SpatialAudioAvailabilityModel.HeadTracking,
+                            )
                         SpatialAudioButtonViewModel(button = buttonViewModel, model = isEnabled)
                     }
             }
@@ -97,11 +117,21 @@
         scope.launch { interactor.setEnabled(model) }
     }
 
-    private fun SpatialAudioEnabledModel.toViewModel(isChecked: Boolean): ButtonViewModel {
+    private fun SpatialAudioEnabledModel.toViewModel(
+        isChecked: Boolean,
+        isHeadTrackingAvailable: Boolean,
+    ): ButtonViewModel {
+        // This method deliberately uses the same icon for the case when head tracking is disabled
+        // to show a toggle button with a non-changing icon
         if (this is SpatialAudioEnabledModel.HeadTrackingEnabled) {
             return ButtonViewModel(
                 isActive = isChecked,
-                icon = Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null),
+                icon =
+                    if (isHeadTrackingAvailable) {
+                        Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null)
+                    } else {
+                        spatialSpeakerIcon
+                    },
                 label = context.getString(R.string.volume_panel_spatial_audio_tracking)
             )
         }
@@ -109,7 +139,12 @@
         if (this is SpatialAudioEnabledModel.SpatialAudioEnabled) {
             return ButtonViewModel(
                 isActive = isChecked,
-                icon = Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null),
+                icon =
+                    if (isHeadTrackingAvailable) {
+                        Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null)
+                    } else {
+                        spatialSpeakerIcon
+                    },
                 label = context.getString(R.string.volume_panel_spatial_audio_fixed)
             )
         }
@@ -117,7 +152,12 @@
         if (this is SpatialAudioEnabledModel.Disabled) {
             return ButtonViewModel(
                 isActive = isChecked,
-                icon = Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null),
+                icon =
+                    if (isHeadTrackingAvailable) {
+                        Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null)
+                    } else {
+                        spatialSpeakerIcon
+                    },
                 label = context.getString(R.string.volume_panel_spatial_audio_off)
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index fd01b48..850162e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -146,14 +146,18 @@
             isEnabled = isEnabled,
             a11yStep = volumeRange.step,
             a11yClickDescription =
-                context.getString(
-                    if (isMuted) {
-                        R.string.volume_panel_hint_unmute
-                    } else {
-                        R.string.volume_panel_hint_mute
-                    },
-                    label,
-                ),
+                if (isAffectedByMute) {
+                    context.getString(
+                        if (isMuted) {
+                            R.string.volume_panel_hint_unmute
+                        } else {
+                            R.string.volume_panel_hint_mute
+                        },
+                        label,
+                    )
+                } else {
+                    null
+                },
             a11yStateDescription =
                 if (volume == volumeRange.first) {
                     context.getString(
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 99f95648..3da725b 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
@@ -98,12 +98,12 @@
 
     private fun showNewVolumePanel() {
         activityStarter.dismissKeyguardThenExecute(
-            {
+            /* action = */ {
                 volumePanelGlobalStateInteractor.setVisible(true)
                 false
             },
-            {},
-            true
+            /* cancel = */ {},
+            /* afterKeyguardGone = */ true,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
index 8c4179d..0a3225e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
@@ -161,8 +161,11 @@
         doAnswer(this::checkMainThread).when(mKeyguardUpdateMonitor)
                 .removeCallback(any(KeyguardUpdateMonitorCallback.class));
 
-        mCarrierTextCallbackInfo = new CarrierTextManager.CarrierTextCallbackInfo("",
-                new CharSequence[]{}, false, new int[]{});
+        mCarrierTextCallbackInfo = new CarrierTextManager.CarrierTextCallbackInfo(
+                /* carrierText= */ "",
+                /* listOfCarriers= */ new CharSequence[]{},
+                /* anySimReady= */ false,
+                /* subscriptionIds= */ new int[]{});
         when(mTelephonyManager.getSupportedModemCount()).thenReturn(3);
         when(mTelephonyManager.getActiveModemCount()).thenReturn(3);
 
@@ -473,7 +476,7 @@
     }
 
     @Test
-    public void carrierText_satelliteTextNull_notUsed() {
+    public void carrierText_satelliteTextNull_isSatelliteFalse_textNotUsed() {
         reset(mCarrierTextCallback);
         List<SubscriptionInfo> list = new ArrayList<>();
         list.add(TEST_SUBSCRIPTION);
@@ -491,12 +494,38 @@
                         CarrierTextManager.CarrierTextCallbackInfo.class);
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
 
-        // THEN the default subscription carrier text is used
+        // THEN satellite mode is false and the default subscription carrier text is used
         verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().isInSatelliteMode).isFalse();
         assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
     }
 
     @Test
+    public void carrierText_hasSatelliteText_isSatelliteTrue_textUsed() {
+        reset(mCarrierTextCallback);
+        List<SubscriptionInfo> list = new ArrayList<>();
+        list.add(TEST_SUBSCRIPTION);
+        when(mKeyguardUpdateMonitor.getSimState(anyInt())).thenReturn(
+                TelephonyManager.SIM_STATE_READY);
+        when(mKeyguardUpdateMonitor.getFilteredSubscriptionInfo()).thenReturn(list);
+        mKeyguardUpdateMonitor.mServiceStates = new HashMap<>();
+
+        // WHEN the satellite text is non-null
+        mSatelliteViewModel.getCarrierText().setValue("Satellite Test Text");
+        mTestScope.getTestScheduler().runCurrent();
+
+        ArgumentCaptor<CarrierTextManager.CarrierTextCallbackInfo> captor =
+                ArgumentCaptor.forClass(
+                        CarrierTextManager.CarrierTextCallbackInfo.class);
+        FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
+
+        // THEN satellite mode is true and the satellite text is used
+        verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().isInSatelliteMode).isTrue();
+        assertThat(captor.getValue().carrierText).isEqualTo("Satellite Test Text");
+    }
+
+    @Test
     public void carrierText_satelliteTextUpdates_autoTriggersCallback() {
         reset(mCarrierTextCallback);
         List<SubscriptionInfo> list = new ArrayList<>();
@@ -517,6 +546,7 @@
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
         verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
         // AND use the satellite text as the carrier text
+        assertThat(captor.getValue().isInSatelliteMode).isTrue();
         assertThat(captor.getValue().carrierText).isEqualTo("Test satellite text");
 
         // WHEN the satellite text is reset to null
@@ -528,6 +558,7 @@
         // that doesn't include the satellite info
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
         verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().isInSatelliteMode).isFalse();
         assertThat(captor.getValue().carrierText).isEqualTo(TEST_CARRIER);
     }
 
@@ -566,10 +597,11 @@
         mCarrierTextManager.setListening(mCarrierTextCallback);
 
         // THEN we should automatically re-trigger #updateCarrierText and get callback info
-        // that includes the new satellite text
+        // that includes the new satellite state and text
         mTestScope.getTestScheduler().runCurrent();
         FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor);
         verify(mCarrierTextCallback).updateCarrierInfo(captor.capture());
+        assertThat(captor.getValue().isInSatelliteMode).isTrue();
         assertThat(captor.getValue().carrierText).isEqualTo("New satellite text");
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 1167fce..f46cfdc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -383,11 +383,25 @@
             }
 
             if (testCase.isFaceOnly) {
-                val expectedIconAsset = R.raw.face_dialog_authenticating
+                val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+                val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+                val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark)
+
+                val expectedIconAsset =
+                    if (shouldPulseAnimation!!) {
+                        if (lastPulseLightToDark!!) {
+                            R.drawable.face_dialog_pulse_dark_to_light
+                        } else {
+                            R.drawable.face_dialog_pulse_light_to_dark
+                        }
+                    } else {
+                        R.drawable.face_dialog_pulse_dark_to_light
+                    }
                 assertThat(iconAsset).isEqualTo(expectedIconAsset)
                 assertThat(iconContentDescriptionId)
                     .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
+                assertThat(shouldRepeatAnimation).isEqualTo(true)
             }
 
             if (testCase.isCoex) {
@@ -409,11 +423,26 @@
                     }
                 } else {
                     // implicit flow
-                    val expectedIconAsset = R.raw.face_dialog_authenticating
+                    val shouldRepeatAnimation by
+                        collectLastValue(iconViewModel.shouldRepeatAnimation)
+                    val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+                    val lastPulseLightToDark by collectLastValue(iconViewModel.lastPulseLightToDark)
+
+                    val expectedIconAsset =
+                        if (shouldPulseAnimation!!) {
+                            if (lastPulseLightToDark!!) {
+                                R.drawable.face_dialog_pulse_dark_to_light
+                            } else {
+                                R.drawable.face_dialog_pulse_light_to_dark
+                            }
+                        } else {
+                            R.drawable.face_dialog_pulse_dark_to_light
+                        }
                     assertThat(iconAsset).isEqualTo(expectedIconAsset)
                     assertThat(iconContentDescriptionId)
                         .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticating)
                     assertThat(shouldAnimateIconView).isEqualTo(true)
+                    assertThat(shouldRepeatAnimation).isEqualTo(true)
                 }
             }
         }
@@ -503,9 +532,14 @@
         }
 
         if (testCase.isFaceOnly) {
+            val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+            val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+            assertThat(shouldPulseAnimation!!).isEqualTo(false)
             assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_error)
             assertThat(iconContentDescriptionId).isEqualTo(R.string.keyguard_face_failed)
             assertThat(shouldAnimateIconView).isEqualTo(true)
+            assertThat(shouldRepeatAnimation).isEqualTo(false)
 
             // Clear error, go to idle
             errorJob.join()
@@ -514,6 +548,7 @@
             assertThat(iconContentDescriptionId)
                 .isEqualTo(R.string.biometric_dialog_face_icon_description_idle)
             assertThat(shouldAnimateIconView).isEqualTo(true)
+            assertThat(shouldRepeatAnimation).isEqualTo(false)
         }
 
         if (testCase.isCoex) {
@@ -596,10 +631,15 @@
 
             // If co-ex, using implicit flow (explicit flow always requires confirmation)
             if (testCase.isFaceOnly || testCase.isCoex) {
+                val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+                val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+                assertThat(shouldPulseAnimation!!).isEqualTo(false)
                 assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark)
                 assertThat(iconContentDescriptionId)
                     .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
+                assertThat(shouldRepeatAnimation).isEqualTo(false)
             }
         }
     }
@@ -621,10 +661,15 @@
             )
 
             if (testCase.isFaceOnly) {
+                val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+                val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+                assertThat(shouldPulseAnimation!!).isEqualTo(false)
                 assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_wink_from_dark)
                 assertThat(iconContentDescriptionId)
                     .isEqualTo(R.string.biometric_dialog_face_icon_description_authenticated)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
+                assertThat(shouldRepeatAnimation).isEqualTo(false)
             }
 
             // explicit flow because confirmation requested
@@ -666,10 +711,15 @@
             viewModel.confirmAuthenticated()
 
             if (testCase.isFaceOnly) {
+                val shouldRepeatAnimation by collectLastValue(iconViewModel.shouldRepeatAnimation)
+                val shouldPulseAnimation by collectLastValue(iconViewModel.shouldPulseAnimation)
+
+                assertThat(shouldPulseAnimation!!).isEqualTo(false)
                 assertThat(iconAsset).isEqualTo(R.drawable.face_dialog_dark_to_checkmark)
                 assertThat(iconContentDescriptionId)
                     .isEqualTo(R.string.biometric_dialog_face_icon_description_confirmed)
                 assertThat(shouldAnimateIconView).isEqualTo(true)
+                assertThat(shouldRepeatAnimation).isEqualTo(false)
             }
 
             // explicit flow because confirmation requested
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
index 84b1c00..87dd9b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsGridLayoutTest.java
@@ -21,11 +21,11 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 
-import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -40,7 +40,7 @@
  * Tests for {@link ListGridLayout}.
  */
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class GlobalActionsGridLayoutTest extends SysuiTestCase {
 
     private GlobalActionsGridLayout mGridLayout;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java
index 16dcd65..ea0f4d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsLayoutTest.java
@@ -24,11 +24,11 @@
 import static org.mockito.Mockito.spy;
 
 import android.content.Context;
-import android.testing.AndroidTestingRunner;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.MultiListLayout;
@@ -44,7 +44,7 @@
  * Tests for {@link ListGridLayout}.
  */
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class GlobalActionsLayoutTest extends SysuiTestCase {
 
     private TestLayout mLayout;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
index 4c65b90..a10457f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ListGridLayoutTest.java
@@ -18,11 +18,11 @@
 
 import static junit.framework.Assert.assertEquals;
 
-import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -36,7 +36,7 @@
  * Tests for {@link ListGridLayout}.
  */
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ListGridLayoutTest extends SysuiTestCase {
 
     private ListGridLayout mListGridLayout;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java
index 28c01ad..73509e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java
@@ -26,8 +26,8 @@
 import android.os.PowerManager;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
-import android.testing.AndroidTestingRunner;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
@@ -41,7 +41,7 @@
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ShutdownUiTest extends SysuiTestCase {
 
     ShutdownUi mShutdownUi;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt
index d7a0d5b5..64cd091 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/domain/interactor/KeyboardBacklightInteractorTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyboard.backlight.domain.interactor
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -29,11 +30,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyboardBacklightInteractorTest : SysuiTestCase() {
 
     private val keyboardRepository = FakeKeyboardRepository()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt
index 7207fbf..47261a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/KeyboardBacklightDialogCoordinatorTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyboard.backlight.ui
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyboard.backlight.domain.interactor.KeyboardBacklightInteractor
@@ -37,7 +38,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
@@ -46,7 +46,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyboardBacklightDialogCoordinatorTest : SysuiTestCase() {
 
     @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index 4410e68..53bcf86 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -21,6 +21,7 @@
 import android.hardware.input.InputManager.KeyboardBacklightListener
 import android.hardware.input.KeyboardBacklightState
 import android.view.InputDevice
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.FlowValue
@@ -44,7 +45,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
@@ -53,7 +53,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyboardRepositoryTest : SysuiTestCase() {
 
     @Captor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
index 05a2ca2..0757ea1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperActivityStarterTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyboard.shortcut.ui
 
 import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyboard.shortcut.fakeShortcutHelperStartActivity
@@ -33,11 +34,10 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class ShortcutHelperActivityStarterTest : SysuiTestCase() {
 
     private val kosmos =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
index 44a8904..34b2aaf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyboard.shortcut.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -34,11 +35,10 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class ShortcutHelperViewModelTest : SysuiTestCase() {
 
     private val kosmos =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
index 59d8fc3..9a721fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyboard.stickykeys.ui
 
 import android.app.Dialog
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyboard.data.repository.FakeStickyKeysRepository
@@ -36,13 +37,12 @@
 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.verifyZeroInteractions
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() {
 
     private lateinit var coordinator: StickyKeysIndicatorCoordinator
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
index d14d72d..9d9e5be62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
@@ -19,6 +19,7 @@
 import android.hardware.input.InputManager
 import android.hardware.input.StickyModifierState
 import android.provider.Settings.Secure.ACCESSIBILITY_STICKY_KEYS
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -47,14 +48,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentCaptor
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class StickyKeysIndicatorViewModelTest : SysuiTestCase() {
 
     private val dispatcher = StandardTestDispatcher()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
index 5b836b6..925ace2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
@@ -36,9 +36,9 @@
 
 import android.content.res.ColorStateList;
 import android.graphics.Color;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.logging.KeyguardLogger;
@@ -56,7 +56,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @RunWithLooper
 @SmallTest
 public class KeyguardIndicationRotateTextViewControllerTest extends SysuiTestCase {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationTest.java
index b44fb8e..325bae1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationTest.java
@@ -24,10 +24,10 @@
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
 import android.graphics.drawable.Drawable;
-import android.testing.AndroidTestingRunner;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -35,7 +35,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @SmallTest
 public class KeyguardIndicationTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
index ce8028c..909acca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardSliceProviderTest.java
@@ -34,7 +34,6 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.provider.Settings;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
 
@@ -44,6 +43,7 @@
 import androidx.slice.SliceSpecs;
 import androidx.slice.builders.ListBuilder;
 import androidx.slice.core.SliceQuery;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -72,7 +72,7 @@
 import java.util.concurrent.TimeUnit;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @RunWithLooper
 public class KeyguardSliceProviderTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 6ebda4d..128dd23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -7,7 +7,6 @@
 import android.graphics.Rect
 import android.os.PowerManager
 import android.platform.test.annotations.DisableFlags
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import android.view.RemoteAnimationTarget
 import android.view.SurfaceControl
@@ -15,6 +14,7 @@
 import android.view.View
 import android.view.ViewRootImpl
 import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.Flags
@@ -46,7 +46,7 @@
 import org.mockito.MockitoAnnotations
 import java.util.function.Predicate
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @RunWithLooper
 @SmallTest
 class KeyguardUnlockAnimationControllerTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
index 977116e..144e634 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt
@@ -1,20 +1,15 @@
 package com.android.systemui.keyguard
 
 import android.content.ComponentCallbacks2
-import android.graphics.HardwareRenderer
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.kosmos.testDispatcher
@@ -26,7 +21,6 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
 import com.android.systemui.utils.GlobalWindowManager
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
@@ -43,7 +37,7 @@
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class ResourceTrimmerTest : SysuiTestCase() {
     val kosmos = testKosmos()
@@ -62,52 +56,21 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, true)
         keyguardRepository.setDozeAmount(0f)
         keyguardRepository.setKeyguardGoingAway(false)
-
-        val withDeps =
-            KeyguardInteractorFactory.create(
-                featureFlags = featureFlags,
-                repository = keyguardRepository,
-            )
-        val keyguardInteractor = withDeps.keyguardInteractor
         resourceTrimmer =
             ResourceTrimmer(
-                keyguardInteractor,
-                powerInteractor = powerInteractor,
                 keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
                 globalWindowManager = globalWindowManager,
                 applicationScope = testScope.backgroundScope,
                 bgDispatcher = kosmos.testDispatcher,
-                featureFlags = featureFlags,
                 sceneInteractor = kosmos.sceneInteractor,
             )
         resourceTrimmer.start()
     }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun noChange_noOutputChanges() =
-        testScope.runTest {
-            testScope.runCurrent()
-            verifyZeroInteractions(globalWindowManager)
-        }
-
-    @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun dozeAodDisabled_sleep_trimsMemory() =
-        testScope.runTest {
-            powerInteractor.setAsleepForTest()
-            testScope.runCurrent()
-            verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
-        }
-
-    @Test
-    @DisableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun dozeAodDisabled_flagDisabled_sleep_doesntTrimMemory() =
+    fun dozeAodDisabled_sleep_doesntTrimMemory() =
         testScope.runTest {
             powerInteractor.setAsleepForTest()
             testScope.runCurrent()
@@ -115,8 +78,7 @@
         }
 
     @Test
-    @DisableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun dozeEnabled_flagDisabled_sleepWithFullDozeAmount_doesntTrimMemory() =
+    fun dozeEnabled_sleepWithFullDozeAmount_doesntTrimMemory() =
         testScope.runTest {
             keyguardRepository.setDreaming(true)
             keyguardRepository.setDozeAmount(1f)
@@ -126,20 +88,6 @@
         }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun dozeEnabled_sleepWithFullDozeAmount_trimsMemory() =
-        testScope.runTest {
-            keyguardRepository.setDreaming(true)
-            keyguardRepository.setDozeAmount(1f)
-            powerInteractor.setAsleepForTest()
-            testScope.runCurrent()
-            verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
-        }
-
-    @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
     fun dozeEnabled_sleepWithoutFullDozeAmount_doesntTrimMemory() =
         testScope.runTest {
             keyguardRepository.setDreaming(true)
@@ -150,34 +98,6 @@
         }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    fun aodEnabled_sleepWithFullDozeAmount_trimsMemoryOnce() {
-        testScope.runTest {
-            keyguardRepository.setDreaming(true)
-            keyguardRepository.setDozeAmount(0f)
-            powerInteractor.setAsleepForTest()
-
-            testScope.runCurrent()
-            verifyZeroInteractions(globalWindowManager)
-
-            generateSequence(0f) { it + 0.1f }
-                .takeWhile { it < 1f }
-                .forEach {
-                    keyguardRepository.setDozeAmount(it)
-                    testScope.runCurrent()
-                }
-            verifyZeroInteractions(globalWindowManager)
-
-            keyguardRepository.setDozeAmount(1f)
-            testScope.runCurrent()
-            verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_ALL)
-        }
-    }
-
-    @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
     fun aodEnabled_deviceWakesHalfWayThrough_doesNotTrimMemory() {
         testScope.runTest {
             keyguardRepository.setDreaming(true)
@@ -209,7 +129,6 @@
     }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
     @DisableSceneContainer
     fun keyguardTransitionsToGone_trimsFontCache() =
         testScope.runTest {
@@ -220,12 +139,10 @@
             )
             verify(globalWindowManager, times(1))
                 .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
             verifyNoMoreInteractions(globalWindowManager)
         }
 
     @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
     @EnableSceneContainer
     fun keyguardTransitionsToGone_trimsFontCache_scene_container() =
         testScope.runTest {
@@ -233,38 +150,6 @@
 
             verify(globalWindowManager, times(1))
                 .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(1)).trimCaches(HardwareRenderer.CACHE_TRIM_FONT)
             verifyNoMoreInteractions(globalWindowManager)
         }
-
-    @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    @DisableSceneContainer
-    fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache() =
-        testScope.runTest {
-            featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
-            keyguardTransitionRepository.sendTransitionSteps(
-                from = KeyguardState.LOCKSCREEN,
-                to = KeyguardState.GONE,
-                testScope
-            )
-            // Memory hidden should still be called.
-            verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(0)).trimCaches(any())
-        }
-
-    @Test
-    @EnableFlags(com.android.systemui.Flags.FLAG_TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)
-    @EnableSceneContainer
-    fun keyguardTransitionsToGone_flagDisabled_doesNotTrimFontCache_scene_container() =
-        testScope.runTest {
-            featureFlags.set(Flags.TRIM_FONT_CACHES_AT_UNLOCK, false)
-            kosmos.setSceneTransition(Idle(Scenes.Gone))
-
-            // Memory hidden should still be called.
-            verify(globalWindowManager, times(1))
-                .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
-            verify(globalWindowManager, times(0)).trimCaches(any())
-        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
index 70a0415..99ff2d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ScreenLifecycleTest.java
@@ -21,8 +21,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-import android.testing.AndroidTestingRunner;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -35,7 +35,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @SmallTest
 public class ScreenLifecycleTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
index 39a453d..a9f7d00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java
@@ -24,8 +24,8 @@
 
 import android.app.IWallpaperManager;
 import android.os.PowerManager;
-import android.testing.AndroidTestingRunner;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -39,7 +39,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @SmallTest
 public class WakefulnessLifecycleTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt
index c7f1dec..d435a47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WorkLockActivityTest.kt
@@ -25,8 +25,8 @@
 import android.os.Looper
 import android.os.UserHandle
 import android.os.UserManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.util.mockito.any
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfigTest.kt
index bd4525b..47bf653 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfigTest.kt
@@ -18,16 +18,16 @@
 
 import android.content.Intent
 import android.net.Uri
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
index df1833e..5fa194d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -203,4 +203,4 @@
         verify(ringerModeInternal).removeObserver(any())
         coroutineContext.cancelChildren()
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index f5b5261..972ca02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,6 +19,7 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
@@ -37,11 +38,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: KeyguardBlueprintRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
index 9aac8e2..af5187d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepositoryTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.ClockEventController
 import com.android.systemui.SysuiTestCase
@@ -36,11 +37,10 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeyguardClockRepositoryTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
index a320845..8b8a6cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepositoryImplTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -31,10 +32,9 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeyguardSmartspaceRepositoryImplTest : SysuiTestCase() {
 
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 8be1e7a..a8271c1 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
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.os.Handler
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -56,7 +57,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.junit.MockitoJUnit
@@ -64,7 +64,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class DeviceEntrySideFpsOverlayInteractorTest : SysuiTestCase() {
     @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 14d9548..d2a9c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -63,11 +63,11 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.runner.parameterized.Parameter
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.eq
@@ -83,7 +83,7 @@
     detail = "on certain architectures all permutations with startActivity=true is causing failures"
 )
 @SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @DisableSceneContainer
 class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index ced3526..9d06031 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -63,11 +63,11 @@
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.runner.parameterized.Parameter
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameter
-import org.junit.runners.Parameterized.Parameters
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.ArgumentMatchers.eq
@@ -83,7 +83,7 @@
     detail = "on certain architectures all permutations with startActivity=true is causing failures"
 )
 @SmallTest
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @EnableSceneContainer
 class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index 8a0613f..baa8ed7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.view.layout
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
@@ -28,7 +29,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.atLeastOnce
@@ -36,7 +36,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeyguardBlueprintCommandListenerTest : SysuiTestCase() {
     private lateinit var keyguardBlueprintCommandListener: KeyguardBlueprintCommandListener
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 344e0fc..9fab0d90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -17,10 +17,10 @@
 
 package com.android.systemui.keyguard.ui.view.layout.blueprints
 
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection
@@ -54,7 +54,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @RunWithLooper(setAsMainLooper = true)
 @ExperimentalCoroutinesApi
 @SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index b3cc5c9..4a39a9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -22,6 +22,7 @@
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -52,12 +53,11 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class ClockSectionTest : SysuiTestCase() {
     private lateinit var underTest: ClockSection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 4f2b690..693a877 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -21,6 +21,7 @@
 import android.view.WindowManager
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.LegacyLockIconViewController
 import com.android.systemui.Flags as AConfigFlags
@@ -44,14 +45,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Answers
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
 @ExperimentalCoroutinesApi
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class DefaultDeviceEntrySectionTest : SysuiTestCase() {
     @Mock private lateinit var authController: AuthController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
index 711f90f..10f7128 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSectionTest.kt
@@ -20,6 +20,7 @@
 import android.view.ViewGroup
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.SysuiTestCase
@@ -30,11 +31,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class DefaultIndicationAreaSectionTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
index be10b82..7d4f034 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt
@@ -22,6 +22,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.GONE
 import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
@@ -41,11 +42,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class SmartspaceSectionTest : SysuiTestCase() {
     private lateinit var underTest: SmartspaceSection
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index f1c93c4..e44bc7b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -35,11 +36,10 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mockito.verify
 
 @ExperimentalCoroutinesApi
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class AlternateBouncerViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt
index 391831a..6398a5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.Flags
@@ -37,11 +38,10 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.kotlin.whenever
 
 @ExperimentalCoroutinesApi
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class AlternateBouncerWindowViewModelTest : SysuiTestCase() {
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
index ec2a1d3..129752e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModelTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.os.fakeExecutorHandler
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
@@ -25,12 +26,11 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class KeyguardBlueprintViewModelTest : SysuiTestCase() {
     @Mock private lateinit var keyguardBlueprintInteractor: KeyguardBlueprintInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 8a12a90..16dd9af 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -20,6 +20,7 @@
 import android.app.admin.DevicePolicyManager
 import android.content.Intent
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.Flags as AConfigFlags
@@ -49,6 +50,7 @@
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
@@ -75,7 +77,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -83,7 +84,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() {
 
     @Mock private lateinit var activityStarter: ActivityStarter
@@ -289,6 +290,7 @@
 
         underTest =
             KeyguardQuickAffordancesCombinedViewModel(
+                applicationScope = kosmos.applicationCoroutineScope,
                 quickAffordanceInteractor =
                     KeyguardQuickAffordanceInteractor(
                         keyguardInteractor = keyguardInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
index 78f4dcc..0c3fcb3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -32,13 +33,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Answers
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardSmartspaceViewModelTest : SysuiTestCase() {
     val kosmos = testKosmos()
     val testScope = kosmos.testScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
index 447b333..fbeb6d8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/SessionTrackerTest.java
@@ -32,9 +32,9 @@
 import static org.mockito.Mockito.when;
 
 import android.os.RemoteException;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.InstanceId;
@@ -54,7 +54,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper
 @SmallTest
 public class SessionTrackerTest extends SysuiTestCase {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt
index ab19b3a..d2e6dad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/core/LoggerTest.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.log.core
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.LogMessageImpl
@@ -16,10 +17,9 @@
 import org.mockito.Mockito.isNull
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
-import org.mockito.junit.MockitoJUnitRunner
 
 @SmallTest
-@RunWith(MockitoJUnitRunner::class)
+@RunWith(AndroidJUnit4::class)
 class LoggerTest : SysuiTestCase() {
     @Mock private lateinit var buffer: MessageBuffer
     private lateinit var message: LogMessage
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
index 5986f4a..e2a2b7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt
@@ -18,8 +18,8 @@
 
 import android.app.smartspace.SmartspaceAction
 import android.os.Bundle
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.systemui.SysuiTestCase
@@ -64,7 +64,7 @@
 private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class LegacyMediaDataFilterImplTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
index dd05a0d..544350c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
@@ -26,9 +26,9 @@
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.logging.InstanceId;
@@ -48,7 +48,7 @@
 import java.util.ArrayList;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper
 public class MediaDataCombineLatestTest extends SysuiTestCase {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index caaa42f..8471fe1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -18,8 +18,8 @@
 
 import android.app.smartspace.SmartspaceAction
 import android.os.Bundle
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.systemui.SysuiTestCase
@@ -76,7 +76,7 @@
 
 @ExperimentalCoroutinesApi
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class MediaDataFilterImplTest : SysuiTestCase() {
     val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 3bf4173..3906c40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -191,7 +191,7 @@
 
     @Before
     fun setup() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         staticMockSession =
             ExtendedMockito.mockitoSession()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 16d8819..42bd46f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -31,8 +31,8 @@
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.annotations.RequiresFlagsDisabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -86,7 +86,7 @@
 private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 public class MediaDeviceManagerTest : SysuiTestCase() {
     @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
index 31a2435..efe4413 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterTest.kt
@@ -20,8 +20,8 @@
 import android.media.session.MediaController.PlaybackInfo
 import android.media.session.MediaSession
 import android.media.session.MediaSessionManager
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.MediaTestUtils
@@ -53,7 +53,7 @@
     MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, notificationKey = NOTIF_KEY)
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 public class MediaSessionBasedFilterTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 6ca0bef..c1bba4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -20,7 +20,7 @@
 import android.media.session.MediaController
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.MediaTestUtils
@@ -65,7 +65,7 @@
 }
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class MediaTimeoutListenerTest : SysuiTestCase() {
 
     @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
index 55ff231..02d7413 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
@@ -27,8 +27,8 @@
 import android.media.MediaDescription
 import android.media.session.MediaSession
 import android.provider.Settings
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -76,7 +76,7 @@
 private fun <T> any(): T = Mockito.any<T>()
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class MediaResumeListenerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserTest.kt
index 8dfa5b8..dca19690 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserTest.kt
@@ -23,8 +23,8 @@
 import android.media.session.MediaController
 import android.media.session.MediaSession
 import android.service.media.MediaBrowserService
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.mock
@@ -53,7 +53,7 @@
 private fun <T> any(): T = Mockito.any<T>()
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 public class ResumeMediaBrowserTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
index b509e77..addf008 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaPlayerDataTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.controls.ui
 
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.MediaTestUtils
@@ -34,7 +34,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 public class MediaPlayerDataTest : SysuiTestCase() {
 
     @Mock private lateinit var playerIsPlaying: MediaControlPanel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/AnimationBindHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/AnimationBindHandlerTest.kt
index 4fcd3bb..cdcb143 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/AnimationBindHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/AnimationBindHandlerTest.kt
@@ -18,8 +18,8 @@
 
 import android.graphics.drawable.Animatable2
 import android.graphics.drawable.Drawable
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import junit.framework.Assert.assertFalse
@@ -37,7 +37,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class AnimationBindHandlerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransitionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransitionTest.kt
index aa297b5..4d0605f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/ColorSchemeTransitionTest.kt
@@ -18,8 +18,8 @@
 
 import android.animation.ValueAnimator
 import android.graphics.Color
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.ui.view.GutsViewHolder
@@ -44,7 +44,7 @@
 private const val TARGET_COLOR = Color.BLUE
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class ColorSchemeTransitionTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/MetadataAnimationHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/MetadataAnimationHandlerTest.kt
index bb95ba3..2499c9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/MetadataAnimationHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/animation/MetadataAnimationHandlerTest.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.media.controls.ui.animation
 
 import android.animation.Animator
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import junit.framework.Assert.fail
@@ -36,7 +36,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class MetadataAnimationHandlerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
index 8a6b272..4e14fec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
@@ -18,11 +18,11 @@
 
 import android.animation.Animator
 import android.animation.ObjectAnimator
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.widget.SeekBar
 import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.ui.drawable.SquigglyProgress
@@ -40,7 +40,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class SeekBarObserverTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt
index 791563a..2f95936 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt
@@ -17,11 +17,11 @@
 package com.android.systemui.media.controls.ui.controller
 
 import android.provider.Settings
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View.GONE
 import android.view.View.VISIBLE
 import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
@@ -50,7 +50,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class KeyguardMediaControllerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index a89139b..3ca5b42 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -22,10 +22,10 @@
 import android.database.ContentObserver
 import android.os.LocaleList
 import android.provider.Settings
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
 import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -40,6 +40,7 @@
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.MediaTestUtils
@@ -107,7 +108,7 @@
 
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class MediaCarouselControllerTest : SysuiTestCase() {
     val kosmos = testKosmos()
 
@@ -158,6 +159,7 @@
         testDispatcher = UnconfinedTestDispatcher()
         mediaCarouselController =
             MediaCarouselController(
+                applicationScope = kosmos.applicationCoroutineScope,
                 context = context,
                 mediaControlPanelFactory = mediaControlPanelFactory,
                 visualStabilityProvider = visualStabilityProvider,
@@ -893,7 +895,10 @@
             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
             mediaCarouselController.mediaCarousel = mediaCarousel
 
-            val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this)
+            val settingsJob =
+                mediaCarouselController.listenForLockscreenSettingChanges(
+                    kosmos.applicationCoroutineScope
+                )
             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false)
 
             val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this)
@@ -920,7 +925,10 @@
             mediaCarouselController.updateHostVisibility = { updatedVisibility = true }
             mediaCarouselController.mediaCarousel = mediaCarousel
 
-            val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this)
+            val settingsJob =
+                mediaCarouselController.listenForLockscreenSettingChanges(
+                    kosmos.applicationCoroutineScope
+                )
             secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true)
 
             val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 6d7976e..ecc456c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -45,7 +45,6 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.provider.Settings
 import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.util.TypedValue
 import android.view.View
@@ -60,6 +59,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.LiveData
 import androidx.media.utils.MediaConstants
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.internal.widget.CachingIconView
@@ -137,7 +137,7 @@
 private const val APP_NAME = "APP_NAME"
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class MediaControlPanelTest : SysuiTestCase() {
     @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
index 5f7c386..bba01bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
@@ -18,10 +18,10 @@
 
 import android.graphics.Rect
 import android.provider.Settings
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.ViewGroup
 import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.SysuiTestCase
@@ -29,7 +29,9 @@
 import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
 import com.android.systemui.dreams.DreamOverlayStateController
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
@@ -49,7 +51,6 @@
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.animation.UniqueObjectHostView
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.settings.FakeSettings
@@ -75,10 +76,12 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.any
+import org.mockito.kotlin.anyOrNull
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 class MediaHierarchyManagerTest : SysuiTestCase() {
 
@@ -112,12 +115,14 @@
     private val testScope = kosmos.testScope
     private lateinit var mediaHierarchyManager: MediaHierarchyManager
     private lateinit var isQsBypassingShade: MutableStateFlow<Boolean>
+    private lateinit var shadeExpansion: MutableStateFlow<Float>
     private lateinit var mediaFrame: ViewGroup
     private val configurationController = FakeConfigurationController()
     private val settings = FakeSettings()
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeHandler: FakeHandler
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val keyguardRepository = kosmos.fakeKeyguardRepository
 
     @Before
     fun setup() {
@@ -129,7 +134,9 @@
         fakeHandler = FakeHandler(testableLooper.looper)
         whenever(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
         isQsBypassingShade = MutableStateFlow(false)
+        shadeExpansion = MutableStateFlow(0f)
         whenever(shadeInteractor.isQsBypassingShade).thenReturn(isQsBypassingShade)
+        whenever(shadeInteractor.shadeExpansion).thenReturn(shadeExpansion)
         whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(false)
         mediaHierarchyManager =
             MediaHierarchyManager(
@@ -141,6 +148,7 @@
                 mediaDataManager,
                 keyguardViewController,
                 dreamOverlayStateController,
+                kosmos.keyguardInteractor,
                 kosmos.communalTransitionViewModel,
                 configurationController,
                 wakefulnessLifecycle,
@@ -191,7 +199,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -204,7 +212,7 @@
         verify(mediaCarouselController, times(0))
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -218,7 +226,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -231,7 +239,7 @@
         verify(mediaCarouselController, times(0))
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -245,7 +253,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -255,7 +263,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -269,7 +277,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -281,7 +289,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 ArgumentMatchers.anyInt(),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 anyBoolean(),
                 anyLong(),
                 anyLong()
@@ -297,7 +305,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 eq(MediaHierarchyManager.LOCATION_QQS),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 eq(false),
                 anyLong(),
                 anyLong()
@@ -309,7 +317,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 eq(MediaHierarchyManager.LOCATION_LOCKSCREEN),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 eq(false),
                 anyLong(),
                 anyLong()
@@ -482,6 +490,26 @@
     }
 
     @Test
+    fun isCurrentlyInGuidedTransformation_desiredLocationIsHub_returnsFalse() =
+        testScope.runTest {
+            goToLockscreen()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+            mediaHierarchyManager.qsExpansion = 0f
+            mediaHierarchyManager.setTransitionToFullShadeAmount(123f)
+
+            whenever(lockHost.visible).thenReturn(true)
+            whenever(qsHost.visible).thenReturn(true)
+            whenever(qqsHost.visible).thenReturn(true)
+            whenever(hubModeHost.visible).thenReturn(true)
+
+            assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse()
+        }
+
+    @Test
     fun testDream() {
         goToDream()
         setMediaDreamComplicationEnabled(true)
@@ -499,7 +527,7 @@
         verify(mediaCarouselController)
             .onDesiredLocationChanged(
                 eq(MediaHierarchyManager.LOCATION_QQS),
-                any(MediaHostState::class.java),
+                any<MediaHostState>(),
                 eq(false),
                 anyLong(),
                 anyLong()
@@ -532,7 +560,7 @@
             verify(mediaCarouselController)
                 .onDesiredLocationChanged(
                     eq(MediaHierarchyManager.LOCATION_QQS),
-                    any(MediaHostState::class.java),
+                    any<MediaHostState>(),
                     eq(false),
                     anyLong(),
                     anyLong()
@@ -590,7 +618,50 @@
             verify(mediaCarouselController)
                 .onDesiredLocationChanged(
                     eq(MediaHierarchyManager.LOCATION_QS),
-                    any(MediaHostState::class.java),
+                    any<MediaHostState>(),
+                    eq(false),
+                    anyLong(),
+                    anyLong()
+                )
+        }
+
+    @Test
+    fun testCommunalLocation_whenDreamingAndShadeExpanding() =
+        testScope.runTest {
+            keyguardRepository.setDreaming(true)
+            runCurrent()
+            keyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.DREAMING,
+                to = KeyguardState.GLANCEABLE_HUB,
+                testScope = testScope,
+            )
+            // Mock the behavior for dreaming that pulling down shade will immediately set QS as
+            // expanded
+            expandQS()
+            // Starts opening the shade
+            shadeExpansion.value = 0.1f
+            runCurrent()
+
+            // UMO shows on hub
+            verify(mediaCarouselController)
+                .onDesiredLocationChanged(
+                    eq(MediaHierarchyManager.LOCATION_COMMUNAL_HUB),
+                    anyOrNull(),
+                    eq(false),
+                    anyLong(),
+                    anyLong()
+                )
+            clearInvocations(mediaCarouselController)
+
+            // The shade is opened enough to make QS elements visible
+            shadeExpansion.value = 0.5f
+            runCurrent()
+
+            // UMO shows on QS
+            verify(mediaCarouselController)
+                .onDesiredLocationChanged(
+                    eq(MediaHierarchyManager.LOCATION_QS),
+                    any<MediaHostState>(),
                     eq(false),
                     anyLong(),
                     anyLong()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
index e5d3082..00b9a46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaViewControllerTest.kt
@@ -23,7 +23,6 @@
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.RippleDrawable
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
@@ -35,6 +34,7 @@
 import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.LiveData
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.SysuiTestCase
@@ -72,7 +72,7 @@
 
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class MediaViewControllerTest : SysuiTestCase() {
     private val mediaHostStateHolder = MediaHost.MediaHostStateHolder()
     private val mediaHostStatesManager = MediaHostStatesManager()
@@ -376,7 +376,7 @@
 
     @Test
     fun attachPlayer_seekBarDisabled_seekBarVisibilityIsSetToInvisible() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -388,7 +388,7 @@
 
     @Test
     fun attachPlayer_seekBarEnabled_seekBarVisible() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -399,7 +399,7 @@
 
     @Test
     fun attachPlayer_seekBarStatusUpdate_seekBarVisibilityChanges() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         getEnabledChangeListener().onEnabledChanged(enabled = true)
@@ -415,7 +415,7 @@
 
     @Test
     fun attachPlayer_notScrubbing_scrubbingViewsGone() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.canShowScrubbingTime = true
@@ -435,7 +435,7 @@
 
     @Test
     fun setIsScrubbing_noSemanticActions_scrubbingViewsGone() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.canShowScrubbingTime = false
@@ -454,7 +454,7 @@
 
     @Test
     fun setIsScrubbing_noPrevButton_scrubbingTimesNotShown() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.setUpNextButtonInfo(true)
@@ -476,7 +476,7 @@
 
     @Test
     fun setIsScrubbing_noNextButton_scrubbingTimesNotShown() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.setUpNextButtonInfo(false)
@@ -498,7 +498,7 @@
 
     @Test
     fun setIsScrubbing_scrubbingViewsShownAndPrevNextHiddenOnlyInExpanded() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.setUpNextButtonInfo(true)
@@ -524,7 +524,7 @@
 
     @Test
     fun setIsScrubbing_trueThenFalse_reservePrevAndNextButtons() {
-        whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true)
+        whenever(mediaFlags.isSceneContainerEnabled()).thenReturn(true)
 
         mediaViewController.attachPlayer(viewHolder)
         mediaViewController.setUpNextButtonInfo(true, ConstraintSet.INVISIBLE)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgressTest.kt
index 0319aaa..e87f176 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgressTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/drawable/SquigglyProgressTest.kt
@@ -21,8 +21,8 @@
 import android.graphics.LightingColorFilter
 import android.graphics.Paint
 import android.graphics.Rect
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.graphics.ColorUtils
 import com.android.systemui.SysuiTestCase
@@ -40,7 +40,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class SquigglyProgressTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
index 1208369..d073cf1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandlerTest.kt
@@ -16,9 +16,9 @@
 
 package com.android.systemui.media.controls.ui.view
 
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.MotionEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.controls.util.MediaUiEventLogger
@@ -38,7 +38,7 @@
 
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class MediaCarouselScrollHandlerTest : SysuiTestCase() {
 
     private val carouselWidth = 1038
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaViewHolderTest.kt
index d3c703c..cdb060c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaViewHolderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/view/MediaViewHolderTest.kt
@@ -16,17 +16,17 @@
 
 package com.android.systemui.media.controls.ui.view
 
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.LayoutInflater
 import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class MediaViewHolderTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt
index e1c2d3f..4da7b2a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModelTest.kt
@@ -20,12 +20,12 @@
 import android.media.session.MediaController
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.MotionEvent
 import android.widget.SeekBar
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.Classifier
@@ -53,7 +53,7 @@
 import org.mockito.junit.MockitoJUnit
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class SeekBarViewModelTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/util/MediaDataUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/util/MediaDataUtilsTest.kt
index 86f3062..bb9d20f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/util/MediaDataUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/util/MediaDataUtilsTest.kt
@@ -16,8 +16,8 @@
 
 package com.android.systemui.media.controls.util
 
-import android.testing.AndroidTestingRunner
 import android.util.Pair as APair
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth.assertThat
@@ -25,7 +25,7 @@
 import org.junit.runner.RunWith
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 class MediaDataUtilsTest : SysuiTestCase() {
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 45ae506..856bc0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -59,12 +59,12 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
-import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.view.View;
 
 import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
@@ -95,7 +95,7 @@
 import java.util.List;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class MediaOutputControllerTest extends SysuiTestCase {
     private static final String TEST_DEVICE_1_ID = "test_device_1_id";
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
index 1e8fbea..50ae25c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogReceiverTest.java
@@ -27,8 +27,8 @@
 import android.content.Intent;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
-import android.testing.AndroidTestingRunner;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.settingslib.flags.Flags;
@@ -40,7 +40,7 @@
 import org.junit.runner.RunWith;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class MediaOutputDialogReceiverTest extends SysuiTestCase {
 
     private MediaOutputDialogReceiver mMediaOutputDialogReceiver;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
index a828843..8e9a1f9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaComplicationViewControllerTest.java
@@ -19,9 +19,9 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 
-import android.testing.AndroidTestingRunner;
 import android.widget.FrameLayout;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -35,7 +35,7 @@
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class MediaComplicationViewControllerTest extends SysuiTestCase {
     @Mock
     private MediaHost mMediaHost;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 8f8630e..a49819e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -25,8 +25,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.testing.AndroidTestingRunner;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -44,7 +44,7 @@
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class MediaDreamSentinelTest extends SysuiTestCase {
     @Mock
     MediaDataManager mMediaDataManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 27f59d29..f1d833f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -23,13 +23,13 @@
 import android.media.MediaRoute2Info
 import android.os.Handler
 import android.os.PowerManager
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.internal.logging.testing.UiEventLoggerFake
@@ -59,7 +59,7 @@
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class MediaTttChipControllerReceiverTest : SysuiTestCase() {
     private lateinit var controllerReceiver: MediaTttChipControllerReceiver
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 3be50b1..111b8d4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -24,7 +24,6 @@
 import android.os.PowerManager
 import android.os.VibrationAttributes
 import android.os.VibrationEffect
-import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
 import android.view.ViewGroup
@@ -32,6 +31,7 @@
 import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.statusbar.IUndoMediaTransferCallback
@@ -74,7 +74,7 @@
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @TestableLooper.RunWithLooper
 class MediaTttSenderCoordinatorTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
index da448aa..6238257 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLoggerTest.kt
@@ -1,7 +1,7 @@
 package com.android.systemui.mediaprojection
 
 import android.media.projection.IMediaProjectionManager
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_APP as METRICS_CREATION_SOURCE_APP
 import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_CAST as METRICS_CREATION_SOURCE_CAST
@@ -13,7 +13,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mockito.verify
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaProjectionMetricsLoggerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
index 2536078..22bdfe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
@@ -2,7 +2,7 @@
 
 import android.content.ComponentName
 import android.os.UserHandle
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
@@ -24,7 +24,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt
index db36131..a73df07 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ActivityTaskManagerThumbnailLoaderTest.kt
@@ -7,9 +7,9 @@
 import android.graphics.Point
 import android.graphics.Rect
 import android.hardware.HardwareBuffer
-import android.testing.AndroidTestingRunner
 import android.view.Surface
 import android.window.TaskSnapshot
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.shared.recents.model.ThumbnailData
@@ -24,7 +24,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
 class ActivityTaskManagerThumbnailLoaderTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt
index fa1c8f8..a0cd835 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/BasicPackageManagerAppIconLoaderTest.kt
@@ -20,6 +20,7 @@
 import android.content.pm.ActivityInfo
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.icons.FastBitmapDrawable
 import com.android.systemui.SysuiTestCase
@@ -31,10 +32,9 @@
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class BasicPackageManagerAppIconLoaderTest : SysuiTestCase() {
 
     private val packageManagerWrapper: PackageManagerWrapper = mock()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
index 6ac86f5..2f61579 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt
@@ -3,7 +3,7 @@
 import android.app.ActivityManager.RecentTaskInfo
 import android.content.pm.UserInfo
 import android.os.UserManager
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.mediaprojection.appselector.data.RecentTask.UserType.CLONED
@@ -25,7 +25,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class ShellRecentTaskListProviderTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index e487780..b7fefc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -17,8 +17,8 @@
 package com.android.systemui.mediaprojection.data.repository
 
 import android.os.Binder
-import android.testing.AndroidTestingRunner
 import android.view.ContentRecordingSession
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -34,7 +34,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaProjectionManagerRepositoryTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
index c63efa1..ea5603d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/devicepolicy/ScreenCaptureDevicePolicyResolverTest.kt
@@ -25,10 +25,11 @@
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Before
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.runner.parameterized.Parameter
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-import org.junit.runners.Parameterized.Parameters
 import org.mockito.ArgumentMatchers.any
 
 abstract class BaseScreenCaptureDevicePolicyResolverTest(private val precondition: Preconditions) :
@@ -81,7 +82,7 @@
     }
 }
 
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 class IsAllowedScreenCaptureDevicePolicyResolverTest(
     private val test: IsScreenCaptureAllowedTestCase
@@ -468,7 +469,7 @@
     }
 }
 
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 @SmallTest
 class IsCompletelyNotAllowedScreenCaptureDevicePolicyResolverTest(
     private val test: IsScreenCaptureCompletelyDisabledTestCase
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
index 16c92ec..8fe8878 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherCoreStartableTest.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.mediaprojection.taskswitcher
 
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_PSS_TASK_SWITCHER
 import com.android.systemui.SysuiTestCase
@@ -27,7 +27,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class MediaProjectionTaskSwitcherCoreStartableTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
index bda0e1e..d3ce871 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/ActivityTaskManagerTasksRepositoryTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.mediaprojection.taskswitcher.data.repository
 
 import android.os.Binder
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -32,7 +32,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class ActivityTaskManagerTasksRepositoryTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
index 33e65f26..7fe55b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractorTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.mediaprojection.taskswitcher.domain.interactor
 
 import android.content.Intent
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -35,7 +35,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class TaskSwitchInteractorTest : SysuiTestCase() {
 
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 ad18099..7417dac 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
@@ -18,7 +18,7 @@
 
 import android.app.Notification
 import android.app.NotificationManager
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.kosmos.testScope
@@ -43,7 +43,7 @@
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
index a468953..5bedc13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/viewmodel/TaskSwitcherNotificationViewModelTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel
 
 import android.content.Intent
-import android.testing.AndroidTestingRunner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -37,7 +37,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
 @SmallTest
 class TaskSwitcherNotificationViewModelTest : SysuiTestCase() {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
index c06a28e..a3be9e3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.model
 
 import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.FakeDisplayTracker
@@ -24,10 +25,9 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class SysUiStateExtTest : SysuiTestCase() {
 
     private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
index f03f4f7..9a78bd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateTest.java
@@ -23,6 +23,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
@@ -32,10 +33,9 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
 
 @SmallTest
-@RunWith(MockitoJUnitRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class SysUiStateTest extends SysuiTestCase {
     private static final int FLAG_1 = 1;
     private static final int FLAG_2 = 1 << 1;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index f1c97dd..23cf7fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -55,6 +55,7 @@
     companion object {
         private const val START_X: Float = 0f
     }
+
     private val kosmos = testKosmos()
     private lateinit var mBackPanelController: BackPanelController
     private lateinit var systemClock: FakeSystemClock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index 6956bea..09d6a1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -39,7 +39,6 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -51,6 +50,7 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
+import androidx.compose.ui.platform.ComposeView;
 import androidx.lifecycle.Lifecycle;
 import androidx.test.filters.SmallTest;
 
@@ -58,12 +58,10 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.EnableSceneContainer;
-import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.media.controls.ui.view.MediaHost;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.dagger.QSComponent;
 import com.android.systemui.qs.external.TileServiceRequestController;
-import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder;
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
@@ -112,9 +110,7 @@
     @Mock private QSSquishinessController mSquishinessController;
     @Mock private FooterActionsViewModel mFooterActionsViewModel;
     @Mock private FooterActionsViewModel.Factory mFooterActionsViewModelFactory;
-    @Mock private FooterActionsViewBinder mFooterActionsViewBinder;
     @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
-    @Mock private FeatureFlagsClassic mFeatureFlags;
     private ViewGroup mQsView;
 
     private final CommandQueue mCommandQueue =
@@ -259,6 +255,39 @@
     }
 
     @Test
+    public void setQsExpansion_whenShouldUpdateSquishinessTrue_setsSquishinessBasedOnFraction() {
+        enableSplitShade();
+        when(mStatusBarStateController.getState()).thenReturn(KEYGUARD);
+        float expansion = 0.456f;
+        float panelExpansionFraction = 0.678f;
+        float proposedTranslation = 567f;
+        float squishinessFraction = 0.789f;
+
+        mUnderTest.setShouldUpdateSquishinessOnMedia(true);
+        mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+                squishinessFraction);
+
+        verify(mQSMediaHost).setSquishFraction(squishinessFraction);
+    }
+
+    @Test
+    public void setQsExpansion_whenOnKeyguardAndShouldUpdateSquishinessFalse_setsSquishiness() {
+        // Random test values without any meaning. They just have to be different from each other.
+        float expansion = 0.123f;
+        float panelExpansionFraction = 0.321f;
+        float proposedTranslation = 456f;
+        float squishinessFraction = 0.567f;
+
+        enableSplitShade();
+        setStatusBarCurrentAndUpcomingState(KEYGUARD);
+        mUnderTest.setShouldUpdateSquishinessOnMedia(false);
+        mUnderTest.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+                squishinessFraction);
+
+        verify(mQSMediaHost).setSquishFraction(1.0f);
+    }
+
+    @Test
     public void setQsExpansion_inSplitShade_setsFooterActionsExpansion_basedOnPanelExpFraction() {
         // Random test values without any meaning. They just have to be different from each other.
         float expansion = 0.123f;
@@ -496,18 +525,13 @@
     @Test
     @EnableSceneContainer
     public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() {
-        clearInvocations(
-                mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory);
+        clearInvocations(mFooterActionsViewModel, mFooterActionsViewModelFactory);
         QSImpl other = instantiate();
 
         other.onComponentCreated(mQsComponent, null);
 
         assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull();
-        verifyZeroInteractions(
-                mFooterActionsViewModel,
-                mFooterActionsViewBinder,
-                mFooterActionsViewModelFactory
-        );
+        verifyZeroInteractions(mFooterActionsViewModel, mFooterActionsViewModelFactory);
     }
 
     @Test
@@ -553,9 +577,7 @@
                 mock(QSLogger.class),
                 mock(FooterActionsController.class),
                 mFooterActionsViewModelFactory,
-                mFooterActionsViewBinder,
-                mLargeScreenShadeInterpolator,
-                mFeatureFlags
+                mLargeScreenShadeInterpolator
         );
     }
 
@@ -589,41 +611,20 @@
         customizer.setId(android.R.id.edit);
         mQsView.addView(customizer);
 
-        View footerActionsView = new FooterActionsViewBinder().create(mContext);
+        ComposeView footerActionsView = new ComposeView(mContext);
         footerActionsView.setId(R.id.qs_footer_actions);
         mQsView.addView(footerActionsView);
     }
 
     private void setUpInflater() {
-        LayoutInflater realInflater = LayoutInflater.from(mContext);
-
         when(mLayoutInflater.cloneInContext(any(Context.class))).thenReturn(mLayoutInflater);
         when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class), anyBoolean()))
-                .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0),
-                        (ViewGroup) invocation.getArgument(1),
-                        (boolean) invocation.getArgument(2)));
+                .thenAnswer((invocation) -> mQsView);
         when(mLayoutInflater.inflate(anyInt(), nullable(ViewGroup.class)))
-                .thenAnswer((invocation) -> inflate(realInflater, (int) invocation.getArgument(0),
-                        (ViewGroup) invocation.getArgument(1)));
+                .thenAnswer((invocation) -> mQsView);
         mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater);
     }
 
-    private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root) {
-        return inflate(realInflater, layoutRes, root, root != null);
-    }
-
-    private View inflate(LayoutInflater realInflater, int layoutRes, @Nullable ViewGroup root,
-            boolean attachToRoot) {
-        if (layoutRes == R.layout.footer_actions
-                || layoutRes == R.layout.footer_actions_text_button
-                || layoutRes == R.layout.footer_actions_number_button
-                || layoutRes == R.layout.footer_actions_icon_button) {
-            return realInflater.inflate(layoutRes, root, attachToRoot);
-        }
-
-        return mQsView;
-    }
-
     private void setupQsComponent() {
         when(mQsComponent.getQSPanelController()).thenReturn(mQSPanelController);
         when(mQsComponent.getQuickQSPanelController()).thenReturn(mQuickQSPanelController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
index 2da4b72..87031d9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/GridConsistencyInteractorTest.kt
@@ -31,9 +31,6 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -46,22 +43,21 @@
 class GridConsistencyInteractorTest : SysuiTestCase() {
 
     private val iconOnlyTiles =
-        MutableStateFlow(
-            setOf(
-                TileSpec.create("smallA"),
-                TileSpec.create("smallB"),
-                TileSpec.create("smallC"),
-                TileSpec.create("smallD"),
-                TileSpec.create("smallE"),
-            )
+        setOf(
+            TileSpec.create("smallA"),
+            TileSpec.create("smallB"),
+            TileSpec.create("smallC"),
+            TileSpec.create("smallD"),
+            TileSpec.create("smallE"),
         )
 
     private val kosmos =
         testKosmos().apply {
             iconTilesRepository =
                 object : IconTilesRepository {
-                    override val iconTilesSpecs: StateFlow<Set<TileSpec>>
-                        get() = iconOnlyTiles.asStateFlow()
+                    override fun isIconTile(spec: TileSpec): Boolean {
+                        return iconOnlyTiles.contains(spec)
+                    }
                 }
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
index bda48ad..1eb6d63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorTest.kt
@@ -25,9 +25,6 @@
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -37,21 +34,20 @@
 class InfiniteGridConsistencyInteractorTest : SysuiTestCase() {
 
     private val iconOnlyTiles =
-        MutableStateFlow(
-            setOf(
-                TileSpec.create("smallA"),
-                TileSpec.create("smallB"),
-                TileSpec.create("smallC"),
-                TileSpec.create("smallD"),
-                TileSpec.create("smallE"),
-            )
+        setOf(
+            TileSpec.create("smallA"),
+            TileSpec.create("smallB"),
+            TileSpec.create("smallC"),
+            TileSpec.create("smallD"),
+            TileSpec.create("smallE"),
         )
     private val kosmos =
         testKosmos().apply {
             iconTilesRepository =
                 object : IconTilesRepository {
-                    override val iconTilesSpecs: StateFlow<Set<TileSpec>>
-                        get() = iconOnlyTiles.asStateFlow()
+                    override fun isIconTile(spec: TileSpec): Boolean {
+                        return iconOnlyTiles.contains(spec)
+                    }
                 }
         }
     private val underTest = with(kosmos) { infiniteGridConsistencyInteractor }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
index db11c3e..196f654 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt
@@ -19,6 +19,8 @@
 import android.content.Context
 import android.graphics.Rect
 import android.graphics.drawable.Drawable
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.service.quicksettings.Tile
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -28,11 +30,13 @@
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.TextView
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS
 import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.haptics.qs.QSLongPressEffect
 import com.android.systemui.haptics.qs.qsLongPressEffect
 import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.qsTileFactory
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -536,10 +540,30 @@
         assertThat(tileView.haveLongPressPropertiesBeenReset).isTrue()
     }
 
+    @Test
+    @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
+    fun onInit_withLongPressEffect_longPressEffectHasTileAndExpandable() {
+        val tile = kosmos.qsTileFactory.createTile("Test Tile")
+        tileView.init(tile)
+
+        assertThat(tileView.isTileAddedToLongPress).isTrue()
+        assertThat(tileView.isExpandableAddedToLongPress).isTrue()
+    }
+
+    @Test
+    @DisableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS)
+    fun onInit_withoutLongPressEffect_longPressEffectDoesNotHaveTileAndExpandable() {
+        val tile = kosmos.qsTileFactory.createTile("Test Tile")
+        tileView.init(tile)
+
+        assertThat(tileView.isTileAddedToLongPress).isFalse()
+        assertThat(tileView.isExpandableAddedToLongPress).isFalse()
+    }
+
     class FakeTileView(
         context: Context,
         collapsed: Boolean,
-        longPressEffect: QSLongPressEffect?,
+        private val longPressEffect: QSLongPressEffect?,
     ) : QSTileViewImpl(
             ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings),
             collapsed,
@@ -547,6 +571,11 @@
     ) {
         var constantLongPressEffectDuration = 500
 
+        val isTileAddedToLongPress: Boolean
+            get() = longPressEffect?.qsTile != null
+        val isExpandableAddedToLongPress: Boolean
+            get() = longPressEffect?.expandable != null
+
         override fun getLongPressEffectDuration(): Int = constantLongPressEffectDuration
         fun changeState(state: QSTile.State) {
             handleStateChanged(state)
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 bde1445..b8267a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -18,6 +18,8 @@
 
 import android.graphics.Rect
 import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.ViewUtils
@@ -30,6 +32,7 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.ambient.touch.TouchHandler
 import com.android.systemui.ambient.touch.TouchMonitor
@@ -51,6 +54,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
@@ -64,9 +68,11 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.Mock
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @ExperimentalCoroutinesApi
@@ -124,6 +130,7 @@
                     ambientTouchComponentFactory,
                     communalContent,
                     kosmos.sceneDataSourceDelegator,
+                    kosmos.notificationStackScrollLayoutController
                 )
         }
         testableLooper = TestableLooper.get(this)
@@ -166,6 +173,7 @@
                         ambientTouchComponentFactory,
                         communalContent,
                         kosmos.sceneDataSourceDelegator,
+                        kosmos.notificationStackScrollLayoutController
                     )
 
                 // First call succeeds.
@@ -176,6 +184,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_communalClosed_doesNotIntercept() =
         with(kosmos) {
@@ -187,6 +196,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_openGesture_interceptsTouches() =
         with(kosmos) {
@@ -204,6 +214,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_communalTransitioning_interceptsTouches() =
         with(kosmos) {
@@ -230,6 +241,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_communalOpen_interceptsTouches() =
         with(kosmos) {
@@ -244,6 +256,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() =
         with(kosmos) {
@@ -262,6 +275,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_communalAndShadeShowing_doesNotIntercept() =
         with(kosmos) {
@@ -278,6 +292,7 @@
             }
         }
 
+    @DisableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
     @Test
     fun onTouchEvent_containerViewDisposed_doesNotIntercept() =
         with(kosmos) {
@@ -310,6 +325,7 @@
                     ambientTouchComponentFactory,
                     communalContent,
                     kosmos.sceneDataSourceDelegator,
+                    kosmos.notificationStackScrollLayoutController,
                 )
 
             assertThat(underTest.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
@@ -329,6 +345,7 @@
                     ambientTouchComponentFactory,
                     communalContent,
                     kosmos.sceneDataSourceDelegator,
+                    kosmos.notificationStackScrollLayoutController,
                 )
 
             // Only initView without attaching a view as we don't want the flows to start collecting
@@ -499,13 +516,30 @@
             }
         }
 
+    @Test
+    @EnableFlags(FLAG_GLANCEABLE_HUB_FULLSCREEN_SWIPE)
+    fun fullScreenSwipeGesture_doNotProcessTouchesInNotificationStack() =
+        with(kosmos) {
+            testScope.runTest {
+                // Communal is closed.
+                goToScene(CommunalScenes.Blank)
+                `when`(
+                        notificationStackScrollLayoutController.isBelowLastNotification(
+                            anyFloat(),
+                            anyFloat()
+                        )
+                    )
+                    .thenReturn(false)
+                assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+            }
+        }
+
     private fun initAndAttachContainerView() {
         containerView = View(context)
 
         parentView = FrameLayout(context)
-        parentView.addView(containerView)
 
-        underTest.initView(containerView)
+        parentView.addView(underTest.initView(containerView))
 
         // Attach the view so that flows start collecting.
         ViewUtils.attachView(parentView, CONTAINER_WIDTH, CONTAINER_HEIGHT)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 041adea..c3cedf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -837,6 +837,7 @@
                 mJavaAdapter,
                 mCastController,
                 new ResourcesSplitShadeStateController(),
+                () -> mKosmos.getCommunalTransitionViewModel(),
                 () -> mLargeScreenHeaderHelper
         );
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 845744a..85541aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -308,6 +308,7 @@
                 new JavaAdapter(mTestScope.getBackgroundScope()),
                 mCastController,
                 splitShadeStateController,
+                () -> mKosmos.getCommunalTransitionViewModel(),
                 () -> mLargeScreenHeaderHelper
         );
         mQsController.init();
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 5363c57..308b370 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
@@ -312,9 +312,10 @@
                 info = new CarrierTextManager.CarrierTextCallbackInfo(
                 "",
                 new CharSequence[]{""},
-                true,
+                /* anySimReady= */ true,
+                /* isInSatelliteMode= */ false,
                 new int[]{0},
-                true /* airplaneMode */);
+                /* airplaneMode= */ true);
         mCallback.updateCarrierInfo(info);
         mTestableLooper.processAllMessages();
         assertEquals(View.GONE, mShadeCarrierGroup.getNoSimTextView().getVisibility());
@@ -326,15 +327,59 @@
                 info = new CarrierTextManager.CarrierTextCallbackInfo(
                 "",
                 new CharSequence[]{FIRST_CARRIER_NAME, ""},
-                true,
+                /* anySimReady= */ true,
+                /* isInSatelliteMode= */ false,
                 new int[]{0, 1},
-                false /* airplaneMode */);
+                /* airplaneMode= */ false);
         mCallback.updateCarrierInfo(info);
         mTestableLooper.processAllMessages();
         assertEquals(View.VISIBLE, mShadeCarrierGroupController.getShadeCarrierVisibility(0));
     }
 
     @Test
+    public void isInSatelliteMode_true_noSimViewShownWithText() {
+        CarrierTextManager.CarrierTextCallbackInfo
+            info = new CarrierTextManager.CarrierTextCallbackInfo(
+            "Satellite Mode Test",
+            new CharSequence[]{FIRST_CARRIER_NAME},
+            /* anySimReady= */ true,
+            /* isInSatelliteMode= */ true,
+            new int[]{1},
+            /* airplaneMode= */ false);
+
+        mCallback.updateCarrierInfo(info);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mShadeCarrierGroup.getNoSimTextView().getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mShadeCarrierGroup.getNoSimTextView().getText()).isEqualTo(
+                "Satellite Mode Test");
+
+        verify(mShadeCarrier1).setVisibility(View.GONE);
+        verify(mShadeCarrier2).setVisibility(View.GONE);
+        verify(mShadeCarrier3).setVisibility(View.GONE);
+    }
+
+    @Test
+    public void isInSatelliteMode_false_normalSimViewsShown() {
+        CarrierTextManager.CarrierTextCallbackInfo
+                info = new CarrierTextManager.CarrierTextCallbackInfo(
+                "Satellite Mode Test",
+                new CharSequence[]{FIRST_CARRIER_NAME, SECOND_CARRIER_NAME},
+                /* anySimReady= */ true,
+                /* isInSatelliteMode= */ false,
+                new int[]{0, 1},
+                /* airplaneMode= */ false);
+
+        mCallback.updateCarrierInfo(info);
+        mTestableLooper.processAllMessages();
+
+        assertThat(mShadeCarrierGroup.getNoSimTextView().getVisibility()).isEqualTo(View.GONE);
+
+        verify(mShadeCarrier1).setVisibility(View.VISIBLE);
+        verify(mShadeCarrier2).setVisibility(View.VISIBLE);
+    }
+
+    @Test
     public void testListenerNotCalledOnRegistreation() {
         mShadeCarrierGroupController
                 .setOnSingleCarrierChangedListener(mOnSingleCarrierChangedListener);
@@ -350,8 +395,7 @@
                 SINGLE_CARRIER_TEXT,
                 new CharSequence[]{SINGLE_CARRIER_TEXT},
                 true,
-                new int[]{0},
-                false /* airplaneMode */);
+                new int[]{0});
 
         mCallback.updateCarrierInfo(info);
         mTestableLooper.processAllMessages();
@@ -369,8 +413,7 @@
                 MULTI_CARRIER_TEXT,
                 new CharSequence[]{FIRST_CARRIER_NAME, SECOND_CARRIER_NAME},
                 true,
-                new int[]{0, 1},
-                false /* airplaneMode */);
+                new int[]{0, 1});
 
         mCallback.updateCarrierInfo(info);
         mTestableLooper.processAllMessages();
@@ -387,16 +430,14 @@
                 SINGLE_CARRIER_TEXT,
                 new CharSequence[]{FIRST_CARRIER_NAME},
                 true,
-                new int[]{0},
-                false /* airplaneMode */);
+                new int[]{0});
 
         CarrierTextManager.CarrierTextCallbackInfo
                 multiCarrierInfo = new CarrierTextManager.CarrierTextCallbackInfo(
                 MULTI_CARRIER_TEXT,
                 new CharSequence[]{FIRST_CARRIER_NAME, SECOND_CARRIER_NAME},
                 true,
-                new int[]{0, 1},
-                false /* airplaneMode */);
+                new int[]{0, 1});
 
         mCallback.updateCarrierInfo(singleCarrierInfo);
         mTestableLooper.processAllMessages();
@@ -421,8 +462,7 @@
                 SINGLE_CARRIER_TEXT,
                 new CharSequence[]{FIRST_CARRIER_NAME},
                 true,
-                new int[]{0},
-                false /* airplaneMode */);
+                new int[]{0});
 
         mCallback.updateCarrierInfo(singleCarrierInfo);
         mTestableLooper.processAllMessages();
@@ -443,8 +483,7 @@
                 MULTI_CARRIER_TEXT,
                 new CharSequence[]{FIRST_CARRIER_NAME, SECOND_CARRIER_NAME},
                 true,
-                new int[]{0, 1},
-                false /* airplaneMode */);
+                new int[]{0, 1});
 
         mCallback.updateCarrierInfo(multiCarrierInfo);
         mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 63ce233..068e166 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -32,6 +32,7 @@
 import android.os.UserHandle
 import android.platform.test.annotations.DisableFlags
 import android.provider.Settings
+import android.testing.TestableLooper.RunWithLooper
 import android.view.View
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
@@ -84,6 +85,7 @@
 import java.util.concurrent.Executor
 
 @SmallTest
+@RunWithLooper(setAsMainLooper = true)
 class LockscreenSmartspaceControllerTest : SysuiTestCase() {
     companion object {
         const val SMARTSPACE_TIME_TOO_EARLY = 1000L
@@ -778,6 +780,7 @@
     }
 
     @Test
+    @RunWithLooper(setAsMainLooper = false)
     fun testConnectAttemptBeforeInitializationShouldNotCreateSession() {
         // GIVEN an uninitalized smartspaceView
         // WHEN the device is provisioned
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 7903a73..e984200 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
@@ -91,7 +91,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -110,7 +111,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldNotHeadsUp(
@@ -129,7 +131,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -146,7 +149,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -163,7 +167,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -180,7 +185,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -197,7 +203,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             assertFsiNotSuppressed()
         }
@@ -208,7 +215,8 @@
         avalancheProvider.startTime = whenAgo(10)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
@@ -232,7 +240,8 @@
         ).thenReturn(PERMISSION_GRANTED)
 
         withFilter(
-            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager)
+            AvalancheSuppressor(avalancheProvider, systemClock, systemSettings, packageManager,
+                    uiEventLogger)
         ) {
             ensurePeekState()
             assertShouldHeadsUp(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index a355cd1..62bffdd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -140,7 +140,7 @@
                 mSmartReplyStateInflater,
                 mNotifLayoutInflaterFactoryProvider,
                 mHeadsUpStyleProvider,
-                mock(NotificationContentInflaterLogger.class));
+                mock(NotificationRowContentBinderLogger.class));
     }
 
     @Test
@@ -265,7 +265,7 @@
                                 R.layout.custom_view_dark);
                     }
                 },
-                mock(NotificationContentInflaterLogger.class));
+                mock(NotificationRowContentBinderLogger.class));
         assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 1661860..65941ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -192,7 +192,7 @@
                 mBgCoroutineContext,
                 mMainCoroutineContext);
 
-        NotificationContentInflater contentBinder = new NotificationContentInflater(
+        NotificationRowContentBinder contentBinder = new NotificationContentInflater(
                 mock(NotifRemoteViewCache.class),
                 mock(NotificationRemoteInputManager.class),
                 mock(ConversationNotificationProcessor.class),
@@ -201,7 +201,7 @@
                 new MockSmartReplyInflater(),
                 mock(NotifLayoutInflaterFactory.Provider.class),
                 mock(HeadsUpStyleProvider.class),
-                mock(NotificationContentInflaterLogger.class));
+                mock(NotificationRowContentBinderLogger.class));
         contentBinder.setInflateSynchronously(true);
         mBindStage = new RowContentBindStage(contentBinder,
                 mock(NotifInflationErrorManager.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
index e025d3d..d366632 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
@@ -372,7 +372,7 @@
         }
 
         // Inflate the SingleLineViewModel
-        // Mock the behavior of NotificationContentInflater.doInBackground
+        // Mock the behavior of NotificationRowContentBinder.doInBackground
         val messagingStyle = builder.getMessagingStyle()
         val isConversation = type is OneToOneConversation || type is GroupConversation
         return SingleLineViewInflater.inflateSingleLineViewModel(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index eb2538e..7d586cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -131,8 +131,9 @@
                 mobileMappings,
                 fakeBroadcastDispatcher,
                 context,
-                IMMEDIATE,
+                /* bgDispatcher = */ IMMEDIATE,
                 scope,
+                /* mainDispatcher = */ IMMEDIATE,
                 FakeAirplaneModeRepository(),
                 wifiRepository,
                 mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 96e599f..76982ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -141,8 +141,8 @@
     private val wifiPickerTrackerCallback =
         argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
 
-    private val dispatcher = StandardTestDispatcher()
-    private val testScope = TestScope(dispatcher)
+    private val testDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
 
     private lateinit var underTest: MobileConnectionsRepositoryImpl
 
@@ -194,7 +194,7 @@
                 flags,
                 testScope.backgroundScope,
                 mainExecutor,
-                dispatcher,
+                testDispatcher,
                 wifiPickerTrackerFactory,
                 wifiManager,
                 wifiLogBuffer,
@@ -216,7 +216,7 @@
                 fakeBroadcastDispatcher,
                 connectivityManager,
                 telephonyManager = telephonyManager,
-                bgDispatcher = dispatcher,
+                bgDispatcher = testDispatcher,
                 logger = logger,
                 mobileMappingsProxy = mobileMappings,
                 scope = testScope.backgroundScope,
@@ -249,8 +249,9 @@
                 mobileMappings,
                 fakeBroadcastDispatcher,
                 context,
-                dispatcher,
+                /* bgDispatcher = */ testDispatcher,
                 testScope.backgroundScope,
+                /* mainDispatcher = */ testDispatcher,
                 airplaneModeRepository,
                 wifiRepository,
                 fullConnectionFactory,
@@ -1225,8 +1226,9 @@
                     mobileMappings,
                     fakeBroadcastDispatcher,
                     context,
-                    dispatcher,
+                    testDispatcher,
                     testScope.backgroundScope,
+                    testDispatcher,
                     airplaneModeRepository,
                     wifiRepository,
                     fullConnectionFactory,
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 02f53b6..d24d87c6 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
@@ -34,6 +34,7 @@
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
 import android.telephony.satellite.SatelliteManager.SatelliteException
 import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -327,7 +328,6 @@
     @Test
     fun satelliteNotSupported_listenersAreNotRegistered() =
         testScope.runTest {
-            setupDefaultRepo()
             // GIVEN satellite is not supported
             setUpRepo(
                 uptime = MIN_UPTIME,
@@ -345,6 +345,110 @@
         }
 
     @Test
+    fun satelliteSupported_registersCallbackForStateChanges() =
+        testScope.runTest {
+            // GIVEN a supported satellite manager.
+            setupDefaultRepo()
+            runCurrent()
+
+            // THEN the repo registers for state changes of satellite support
+            verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+        }
+
+    @Test
+    fun satelliteNotSupported_registersCallbackForStateChanges() =
+        testScope.runTest {
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+
+            runCurrent()
+            // THEN the repo registers for state changes of satellite support
+            verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+        }
+
+    @Test
+    fun satelliteSupportedStateChangedCallbackThrows_doesNotCrash() =
+        testScope.runTest {
+            // GIVEN, satellite manager throws when registering for supported state changes
+            whenever(satelliteManager.registerForSupportedStateChanged(any(), any()))
+                .thenThrow(IllegalStateException())
+
+            // GIVEN a supported satellite manager.
+            setupDefaultRepo()
+            runCurrent()
+
+            // THEN a listener for satellite supported changed can attempt to register,
+            // with no crash
+            verify(satelliteManager).registerForSupportedStateChanged(any(), any())
+        }
+
+    @Test
+    fun satelliteSupported_supportIsLost_unregistersListeners() =
+        testScope.runTest {
+            // GIVEN a supported satellite manager.
+            setupDefaultRepo()
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteSupportedStateCallback> {
+                    verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+                }
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the listeners are registered
+            verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+
+            // WHEN satellite support turns off
+            callback.onSatelliteSupportedStateChanged(false)
+            runCurrent()
+
+            // THEN listeners are unregistered
+            verify(satelliteManager, times(1)).unregisterForModemStateChanged(any())
+            verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any())
+        }
+
+    @Test
+    fun satelliteNotSupported_supportShowsUp_registersListeners() =
+        testScope.runTest {
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteSupportedStateCallback> {
+                    verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+                }
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the listeners are not yet registered
+            verify(satelliteManager, times(0)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(0)).registerForNtnSignalStrengthChanged(any(), any())
+
+            // WHEN satellite support turns on
+            callback.onSatelliteSupportedStateChanged(true)
+            runCurrent()
+
+            // THEN listeners are registered
+            verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+        }
+
+    @Test
     fun repoDoesNotCheckForSupportUntilMinUptime() =
         testScope.runTest {
             // GIVEN we init 100ms after sysui starts up
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt
index a05b5e6..ad5242e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt
@@ -19,7 +19,7 @@
 import android.hardware.display.BrightnessInfo
 import android.hardware.display.BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE
 import android.hardware.display.BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-import com.android.systemui.brightness.data.model.LinearBrightness
+import com.android.systemui.brightness.shared.model.LinearBrightness
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.map
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
index 22784e4..0e84273 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractorKosmos.kt
@@ -18,6 +18,15 @@
 
 import com.android.systemui.brightness.data.repository.screenBrightnessRepository
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.util.mockito.mock
 
 val Kosmos.screenBrightnessInteractor by
-    Kosmos.Fixture { ScreenBrightnessInteractor(screenBrightnessRepository) }
+    Kosmos.Fixture {
+        ScreenBrightnessInteractor(
+            screenBrightnessRepository,
+            applicationCoroutineScope,
+            mock<TableLogBuffer>(),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index b862078..0b28e3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.ui.viewmodel.communalTransitionViewModel
 import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
 import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
@@ -87,6 +88,7 @@
     val configurationInteractor by lazy { kosmos.configurationInteractor }
     val bouncerRepository by lazy { kosmos.bouncerRepository }
     val communalRepository by lazy { kosmos.fakeCommunalSceneRepository }
+    val communalTransitionViewModel by lazy { kosmos.communalTransitionViewModel }
     val headsUpNotificationInteractor by lazy { kosmos.headsUpNotificationInteractor }
     val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
     val keyguardBouncerRepository by lazy { kosmos.fakeKeyguardBouncerRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
index 604c16f..5ff44e5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/QSPipelineRepositoryKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.qs.pipeline.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.retail.data.repository.FakeRetailModeRepository
+import com.android.systemui.retail.data.repository.RetailModeRepository
 
 /** This fake uses 0 as the minimum number of tiles. That means that no tiles is a valid state. */
 var Kosmos.fakeMinimumTilesRepository by Kosmos.Fixture { MinimumTilesFixedRepository(0) }
@@ -46,3 +48,6 @@
 val Kosmos.fakeCustomTileAddedRepository by Kosmos.Fixture { FakeCustomTileAddedRepository() }
 var Kosmos.customTileAddedRepository: CustomTileAddedRepository by
     Kosmos.Fixture { fakeCustomTileAddedRepository }
+
+val Kosmos.fakeRetailModeRepository by Kosmos.Fixture { FakeRetailModeRepository() }
+var Kosmos.retailModeRepository: RetailModeRepository by Kosmos.Fixture { fakeRetailModeRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt
index b870039..d97a5b2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository
 import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.retailModeRepository
 import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
 import com.android.systemui.qs.pipeline.shared.logging.qsLogger
 import com.android.systemui.qs.pipeline.shared.pipelineFlagsRepository
@@ -39,6 +40,7 @@
             installedTilesRepository,
             userRepository,
             minimumTilesRepository,
+            retailModeRepository,
             customTileStatePersister,
             { newQSTileFactory },
             qsTileFactory,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
index 297d1d8..0a3a2ee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.shade.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.util.mockito.mock
@@ -25,7 +25,8 @@
 val Kosmos.shadeLockscreenInteractor by
     Kosmos.Fixture {
         ShadeLockscreenInteractorImpl(
-            scope = testScope,
+            applicationScope = applicationCoroutineScope,
+            backgroundScope = applicationCoroutineScope,
             shadeInteractor = shadeInteractorImpl,
             sceneInteractor = sceneInteractor,
             lockIconViewController = mock(),
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt
index 0854e93..569429f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerKosmos.kt
@@ -13,14 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.dreams.homecontrols
 
-import android.app.Activity
-import android.service.dreams.DreamService
-import javax.inject.Inject
+package com.android.systemui.statusbar.notification.stack
 
-class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider {
-    override fun getActivity(dreamService: DreamService): Activity {
-        return dreamService.activity
-    }
-}
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.notificationStackScrollLayoutController by
+    Kosmos.Fixture { mock<NotificationStackScrollLayoutController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
index 3ac7129..15ef26d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/TestAudioDevicesFactory.kt
@@ -33,19 +33,22 @@
         )
     }
 
-    fun wiredDevice(deviceName: String = "wired"): AudioDeviceInfo {
+    fun wiredDevice(
+        deviceName: String = "wired",
+        deviceAddress: String = "card=1;device=0",
+    ): AudioDeviceInfo {
         return AudioDeviceInfo(
             AudioDevicePort.createForTesting(
                 AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
                 deviceName,
-                "",
+                deviceAddress,
             )
         )
     }
 
     fun bluetoothDevice(
         deviceName: String = "bt",
-        deviceAddress: String = "test_address",
+        deviceAddress: String = "00:43:A8:23:10:F0",
     ): AudioDeviceInfo {
         return AudioDeviceInfo(
             AudioDevicePort.createForTesting(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt
index 3f51a79..e2d414e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.domain.interactor
 
+import android.content.applicationContext
 import com.android.systemui.bluetooth.bluetoothAdapter
 import com.android.systemui.bluetooth.localBluetoothManager
 import com.android.systemui.kosmos.Kosmos
@@ -27,6 +28,7 @@
 val Kosmos.audioOutputInteractor by
     Kosmos.Fixture {
         AudioOutputInteractor(
+            applicationContext,
             audioRepository,
             audioModeInteractor,
             testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
index 9c902cf..680535d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.volume.panel.component.mediaoutput.data.repository
 
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
+import kotlinx.coroutines.CoroutineScope
 
 class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) :
     LocalMediaRepositoryFactory {
@@ -27,6 +28,8 @@
         repositories[packageName] = localMediaRepository
     }
 
-    override fun create(packageName: String?): LocalMediaRepository =
-        repositories[packageName] ?: defaultProvider()
+    override fun create(
+        packageName: String?,
+        coroutineScope: CoroutineScope
+    ): LocalMediaRepository = repositories[packageName] ?: defaultProvider()
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.kt
new file mode 100644
index 0000000..9f11822
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputComponentInteractorKosmos.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.volume.panel.component.mediaoutput.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.volume.domain.interactor.audioModeInteractor
+import com.android.systemui.volume.domain.interactor.audioOutputInteractor
+import com.android.systemui.volume.mediaDeviceSessionInteractor
+import com.android.systemui.volume.mediaOutputInteractor
+
+val Kosmos.mediaOutputComponentInteractor by
+    Kosmos.Fixture {
+        MediaOutputComponentInteractor(
+            testScope.backgroundScope,
+            mediaDeviceSessionInteractor,
+            audioOutputInteractor,
+            audioModeInteractor,
+            mediaOutputInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelKosmos.kt
index 6d4576e..2cd6ff2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelKosmos.kt
@@ -20,11 +20,8 @@
 import com.android.internal.logging.uiEventLogger
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.volume.domain.interactor.audioModeInteractor
-import com.android.systemui.volume.domain.interactor.audioOutputInteractor
-import com.android.systemui.volume.mediaDeviceSessionInteractor
 import com.android.systemui.volume.mediaOutputActionsInteractor
-import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaOutputComponentInteractor
 
 var Kosmos.mediaOutputViewModel by
     Kosmos.Fixture {
@@ -32,10 +29,7 @@
             applicationContext,
             testScope.backgroundScope,
             mediaOutputActionsInteractor,
-            mediaDeviceSessionInteractor,
-            audioOutputInteractor,
-            audioModeInteractor,
-            mediaOutputInteractor,
+            mediaOutputComponentInteractor,
             uiEventLogger,
         )
     }
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 004f37c..2c4bc7c 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -2514,7 +2514,7 @@
         public Plane[] getPlanes() {
             throwISEIfImageIsInvalid();
             if (mPlanes == null) {
-                int fenceFd = mParcelImage.fence != null ? mParcelImage.fence.getFd() : -1;
+                int fenceFd = mParcelImage.fence != null ? mParcelImage.fence.detachFd() : -1;
                 mGraphicBuffer = GraphicBuffer.createFromHardwareBuffer(mParcelImage.buffer);
                 mPlanes = ImageReader.initializeImagePlanes(mParcelImage.planeCount, mGraphicBuffer,
                         fenceFd, mParcelImage.format, mParcelImage.timestamp,
diff --git a/packages/services/VirtualCamera/OWNERS b/packages/services/VirtualCamera/OWNERS
deleted file mode 100644
index c66443f..0000000
--- a/packages/services/VirtualCamera/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-include /services/companion/java/com/android/server/companion/virtual/OWNERS
-caen@google.com
-jsebechlebsky@google.com
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 42f168b..7342b00 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -1379,6 +1379,30 @@
         }
     }
 
+    @RequiresNoPermission
+    @Override
+    public boolean isMagnificationSystemUIConnected() {
+        if (svcConnTracingEnabled()) {
+            logTraceSvcConn("isMagnificationSystemUIConnected", "");
+        }
+        synchronized (mLock) {
+            if (!hasRightsToCurrentUserLocked()) {
+                return false;
+            }
+            if (!mSecurityPolicy.canControlMagnification(this)) {
+                return false;
+            }
+            final long identity = Binder.clearCallingIdentity();
+            try {
+                MagnificationProcessor magnificationProcessor =
+                        mSystemSupport.getMagnificationProcessor();
+                return magnificationProcessor.isMagnificationSystemUIConnected();
+            } finally {
+                Binder.restoreCallingIdentity(identity);
+            }
+        }
+    }
+
     public boolean isMagnificationCallbackEnabled(int displayId) {
         return mInvocationHandler.isMagnificationCallbackEnabled(displayId);
     }
@@ -1724,9 +1748,7 @@
     }
 
     public void resetLocked() {
-        if (Flags.resettableDynamicProperties()) {
-            mAccessibilityServiceInfo.resetDynamicallyConfigurableProperties();
-        }
+        mAccessibilityServiceInfo.resetDynamicallyConfigurableProperties();
         mSystemSupport.getKeyEventDispatcher().flush(this);
         try {
             // Clear the proxy in the other process so this
@@ -1925,6 +1947,11 @@
                 InvocationHandler.MSG_CLEAR_ACCESSIBILITY_CACHE);
     }
 
+    public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+        mInvocationHandler
+                .notifyMagnificationSystemUIConnectionChangedLocked(connected);
+    }
+
     public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
             @NonNull MagnificationConfig config) {
         mInvocationHandler
@@ -1976,6 +2003,21 @@
         return (mGenericMotionEventSources & eventSourceWithoutClass) != 0;
     }
 
+    /**
+     * Called by the invocation handler to notify the service that the
+     * magnification systemui connection has changed.
+     */
+    private void notifyMagnificationSystemUIConnectionChangedInternal(boolean connected) {
+        final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
+        if (listener != null) {
+            try {
+                listener.onMagnificationSystemUIConnectionChanged(connected);
+            } catch (RemoteException re) {
+                Slog.e(LOG_TAG,
+                        "Error sending magnification sysui connection changes to " + mService, re);
+            }
+        }
+    }
 
     /**
      * Called by the invocation handler to notify the service that the
@@ -2372,6 +2414,7 @@
         private static final int MSG_BIND_INPUT = 12;
         private static final int MSG_UNBIND_INPUT = 13;
         private static final int MSG_START_INPUT = 14;
+        private static final int MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED = 15;
 
         /** List of magnification callback states, mapping from displayId -> Boolean */
         @GuardedBy("mlock")
@@ -2398,6 +2441,13 @@
                     notifyClearAccessibilityCacheInternal();
                 } break;
 
+                case MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED: {
+                    final SomeArgs args = (SomeArgs) message.obj;
+                    final boolean connected = args.argi1 == 1;
+                    notifyMagnificationSystemUIConnectionChangedInternal(connected);
+                    args.recycle();
+                } break;
+
                 case MSG_ON_MAGNIFICATION_CHANGED: {
                     final SomeArgs args = (SomeArgs) message.obj;
                     final Region region = (Region) args.arg1;
@@ -2455,6 +2505,15 @@
             }
         }
 
+        public void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.argi1 = connected ? 1 : 0;
+
+            final Message msg =
+                    obtainMessage(MSG_ON_MAGNIFICATION_SYSTEM_UI_CONNECTION_CHANGED, args);
+            msg.sendToTarget();
+        }
+
         public void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
                 @NonNull MagnificationConfig config) {
             synchronized (mLock) {
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 20b727c..d09cb3e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1812,6 +1812,17 @@
     }
 
     /**
+     * Called by the MagnificationController when the magnification systemui connection changes.
+     *
+     * @param connected Whether the connection is ready.
+     */
+    public void notifyMagnificationSystemUIConnectionChanged(boolean connected) {
+        synchronized (mLock) {
+            notifyMagnificationSystemUIConnectionChangedLocked(connected);
+        }
+    }
+
+    /**
      * Called by the MagnificationController when the state of display
      * magnification changes.
      *
@@ -2243,6 +2254,14 @@
         mProxyManager.clearCacheLocked();
     }
 
+    private void notifyMagnificationSystemUIConnectionChangedLocked(boolean connected) {
+        final AccessibilityUserState state = getCurrentUserStateLocked();
+        for (int i = state.mBoundServices.size() - 1; i >= 0; i--) {
+            final AccessibilityServiceConnection service = state.mBoundServices.get(i);
+            service.notifyMagnificationSystemUIConnectionChangedLocked(connected);
+        }
+    }
+
     private void notifyMagnificationChangedLocked(int displayId, @NonNull Region region,
             @NonNull MagnificationConfig config) {
         final AccessibilityUserState state = getCurrentUserStateLocked();
@@ -5253,13 +5272,9 @@
 
                 //Clip to the window bounds.
                 Rect windowBounds = mTempRect1;
-                if (Flags.focusClickPointWindowBoundsFromA11yWindowInfo()) {
-                    AccessibilityWindowInfo window = focus.getWindow();
-                    if (window != null) {
-                        window.getBoundsInScreen(windowBounds);
-                    }
-                } else {
-                    getWindowBounds(focus.getWindowId(), windowBounds);
+                AccessibilityWindowInfo window = focus.getWindow();
+                if (window != null) {
+                    window.getBoundsInScreen(windowBounds);
                 }
                 if (!boundsInScreenBeforeMagnification.intersect(windowBounds)) {
                     return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 4cb3d24..420bac7 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -489,6 +489,14 @@
     /** @throws UnsupportedOperationException since a proxy does not need magnification */
     @RequiresNoPermission
     @Override
+    public boolean isMagnificationSystemUIConnected() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException("isMagnificationSystemUIConnected is not"
+                + " supported");
+    }
+
+    /** @throws UnsupportedOperationException since a proxy does not need magnification */
+    @RequiresNoPermission
+    @Override
     public boolean isMagnificationCallbackEnabled(int displayId) {
         throw new UnsupportedOperationException("isMagnificationCallbackEnabled is not supported");
     }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
index 0719eba..7f4c808 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationConnectionManager.java
@@ -126,6 +126,7 @@
 
     @ConnectionState
     private int mConnectionState = DISCONNECTED;
+    ConnectionStateChangedCallback mConnectionStateChangedCallback = null;
 
     private static final int WAIT_CONNECTION_TIMEOUT_MILLIS = 100;
 
@@ -264,6 +265,9 @@
                 }
             }
         }
+        if (mConnectionStateChangedCallback != null) {
+            mConnectionStateChangedCallback.onConnectionStateChanged(connection != null);
+        }
     }
 
     /**
@@ -271,7 +275,7 @@
      */
     public boolean isConnected() {
         synchronized (mLock) {
-            return mConnectionWrapper != null;
+            return mConnectionWrapper != null && mConnectionState == CONNECTED;
         }
     }
 
@@ -1344,4 +1348,8 @@
             }
         }
     }
+
+    interface ConnectionStateChangedCallback {
+        void onConnectionStateChanged(boolean connected);
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 76367a2..9b78847 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -828,6 +828,8 @@
                 mMagnificationConnectionManager = new MagnificationConnectionManager(mContext,
                         mLock, this, mAms.getTraceManager(),
                         mScaleProvider);
+                mMagnificationConnectionManager.mConnectionStateChangedCallback =
+                        mAms::notifyMagnificationSystemUIConnectionChanged;
             }
             return mMagnificationConnectionManager;
         }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
index ed8f1ab..6036839 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationProcessor.java
@@ -147,6 +147,10 @@
         return false;
     }
 
+    public boolean isMagnificationSystemUIConnected() {
+        return mController.getMagnificationConnectionManager().isConnected();
+    }
+
     private boolean setScaleAndCenterForFullScreenMagnification(int displayId, float scale,
             float centerX, float centerY, boolean animate, int id) {
 
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 0d0c21d..30e4a3e 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -34,6 +34,7 @@
 import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature;
 import static com.android.server.companion.utils.PackageUtils.getPackageInfo;
 import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
+import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
@@ -334,6 +335,12 @@
             enforceCallerCanManageAssociationsForPackage(getContext(), userId, packageName,
                     "get associations");
 
+            if (!checkCallerCanManageCompanionDevice(getContext())) {
+                // If the caller neither is system nor holds MANAGE_COMPANION_DEVICES: it needs to
+                // request the feature (also: the caller is the app itself).
+                enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
+            }
+
             return mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
         }
 
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index 3fbd856..d09d7e6 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -347,8 +347,6 @@
      * Set association tag.
      */
     public void setAssociationTag(int associationId, String tag) {
-        Slog.i(TAG, "Setting association tag=[" + tag + "] to id=[" + associationId + "]...");
-
         AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
                 associationId);
         association = (new AssociationInfo.Builder(association)).setTag(tag).build();
diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java
index 757abd9..29e8095 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationStore.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java
@@ -18,7 +18,7 @@
 
 import static com.android.server.companion.utils.MetricUtils.logCreateAssociation;
 import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation;
-import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -457,10 +457,6 @@
 
     /**
      * Get association by id with caller checks.
-     *
-     * If the association is not found, an IllegalArgumentException would be thrown.
-     *
-     * If the caller can't access the association, a SecurityException would be thrown.
      */
     @NonNull
     public AssociationInfo getAssociationWithCallerChecks(int associationId) {
@@ -470,9 +466,13 @@
                     "getAssociationWithCallerChecks() Association id=[" + associationId
                             + "] doesn't exist.");
         }
-        enforceCallerCanManageAssociationsForPackage(mContext, association.getUserId(),
-                association.getPackageName(), null);
-        return association;
+        if (checkCallerCanManageAssociationsForPackage(mContext, association.getUserId(),
+                association.getPackageName())) {
+            return association;
+        }
+
+        throw new IllegalArgumentException(
+                "The caller can't interact with the association id=[" + associationId + "].");
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
index 6f0baef..8c1116b 100644
--- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
@@ -98,6 +98,7 @@
         Slog.i(TAG, "Disassociating id=[" + id + "]...");
 
         final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id);
+
         final int userId = association.getUserId();
         final String packageName = association.getPackageName();
         final String deviceProfile = association.getDeviceProfile();
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index 00e049c..026d29c 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -122,6 +122,7 @@
      */
     public boolean isPermissionTransferUserConsented(int associationId) {
         mAssociationStore.getAssociationWithCallerChecks(associationId);
+
         PermissionSyncRequest request = getPermissionSyncRequest(associationId);
         if (request == null) {
             return false;
@@ -146,12 +147,12 @@
             return null;
         }
 
-        Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
-                + "] associationId [" + associationId + "]");
-
         final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
                 associationId);
 
+        Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
+                + "] associationId [" + associationId + "]");
+
         // Create an internal intent to launch the user consent dialog
         final Bundle extras = new Bundle();
         PermissionSyncRequest request = new PermissionSyncRequest(associationId);
@@ -219,9 +220,7 @@
      * Enable perm sync for the association
      */
     public void enablePermissionsSync(int associationId) {
-        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
-                associationId);
-        int userId = association.getUserId();
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
         PermissionSyncRequest request = new PermissionSyncRequest(associationId);
         request.setUserConsented(true);
         mSystemDataTransferRequestStore.writeRequest(userId, request);
@@ -231,9 +230,7 @@
      * Disable perm sync for the association
      */
     public void disablePermissionsSync(int associationId) {
-        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
-                associationId);
-        int userId = association.getUserId();
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
         PermissionSyncRequest request = new PermissionSyncRequest(associationId);
         request.setUserConsented(false);
         mSystemDataTransferRequestStore.writeRequest(userId, request);
@@ -244,9 +241,8 @@
      */
     @Nullable
     public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
-        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
-                associationId);
-        int userId = association.getUserId();
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId)
+                .getUserId();
         List<SystemDataTransferRequest> requests =
                 mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
                         associationId);
@@ -263,9 +259,7 @@
      */
     public void removePermissionSyncRequest(int associationId) {
         Binder.withCleanCallingIdentity(() -> {
-            AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
-                    associationId);
-            int userId = association.getUserId();
+            int userId = mAssociationStore.getAssociationById(associationId).getUserId();
             mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, associationId);
         });
     }
diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
index 796d285..f397814 100644
--- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
@@ -149,6 +149,21 @@
     }
 
     /**
+     * Check if the caller is system UID or the provided user.
+     */
+    public static boolean checkCallerIsSystemOr(@UserIdInt int userId,
+            @NonNull String packageName) {
+        final int callingUid = getCallingUid();
+        if (callingUid == SYSTEM_UID) return true;
+
+        if (getCallingUserId() != userId) return false;
+
+        if (!checkPackage(callingUid, packageName)) return false;
+
+        return true;
+    }
+
+    /**
      * Check if the calling user id matches the userId, and if the package belongs to
      * the calling uid.
      */
@@ -169,30 +184,21 @@
     }
 
     /**
+     * Check if the caller holds the necessary permission to manage companion devices.
+     */
+    public static boolean checkCallerCanManageCompanionDevice(@NonNull Context context) {
+        if (getCallingUid() == SYSTEM_UID) return true;
+
+        return context.checkCallingPermission(MANAGE_COMPANION_DEVICES) == PERMISSION_GRANTED;
+    }
+
+    /**
      * Require the caller to be able to manage the associations for the package.
      */
     public static void enforceCallerCanManageAssociationsForPackage(@NonNull Context context,
             @UserIdInt int userId, @NonNull String packageName,
             @Nullable String actionDescription) {
-        final int callingUid = getCallingUid();
-
-        // If the caller is the system
-        if (callingUid == SYSTEM_UID) {
-            return;
-        }
-
-        // If caller can manage the package or has the permissions to manage companion devices
-        boolean canInteractAcrossUsers = context.checkCallingPermission(INTERACT_ACROSS_USERS)
-                == PERMISSION_GRANTED;
-        boolean canManageCompanionDevices = context.checkCallingPermission(MANAGE_COMPANION_DEVICES)
-                == PERMISSION_GRANTED;
-        if (getCallingUserId() == userId) {
-            if (checkPackage(callingUid, packageName) || canManageCompanionDevices) {
-                return;
-            }
-        } else if (canInteractAcrossUsers && canManageCompanionDevices) {
-            return;
-        }
+        if (checkCallerCanManageAssociationsForPackage(context, userId, packageName)) return;
 
         throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
                 + "permissions to "
@@ -213,6 +219,25 @@
         }
     }
 
+    /**
+     * Check if the caller is either:
+     * <ul>
+     * <li> the package itself
+     * <li> the System ({@link android.os.Process#SYSTEM_UID})
+     * <li> holds {@link Manifest.permission#MANAGE_COMPANION_DEVICES} and, if belongs to a
+     * different user, also holds {@link Manifest.permission#INTERACT_ACROSS_USERS}.
+     * </ul>
+     * @return whether the caller is one of the above.
+     */
+    public static boolean checkCallerCanManageAssociationsForPackage(@NonNull Context context,
+            @UserIdInt int userId, @NonNull String packageName) {
+        if (checkCallerIsSystemOr(userId, packageName)) return true;
+
+        if (!checkCallerCanInteractWithUserId(context, userId)) return false;
+
+        return checkCallerCanManageCompanionDevice(context);
+    }
+
     private static boolean checkPackage(@UserIdInt int uid, @NonNull String packageName) {
         try {
             return getAppOpsService().checkPackage(uid, packageName) == MODE_ALLOWED;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 215f640..4a99007 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -29,6 +29,7 @@
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS;
 import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
+import static android.companion.virtualdevice.flags.Flags.intentInterceptionActionMatchingFix;
 
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -1478,7 +1479,13 @@
         synchronized (mVirtualDeviceLock) {
             boolean hasInterceptedIntent = false;
             for (Map.Entry<IBinder, IntentFilter> interceptor : mIntentInterceptors.entrySet()) {
-                if (interceptor.getValue().match(
+                IntentFilter intentFilter = interceptor.getValue();
+                // Explicitly match the actions because the intent filter will match any intent
+                // without an explicit action. If the intent has no action, then require that there
+                // are no actions specified in the filter either.
+                boolean explicitActionMatch = !intentInterceptionActionMatchingFix()
+                        || intent.getAction() != null || intentFilter.countActions() == 0;
+                if (explicitActionMatch && intentFilter.match(
                         intent.getAction(), intent.getType(), intent.getScheme(), intent.getData(),
                         intent.getCategories(), TAG) >= 0) {
                     try {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 0fdf6d0..f1339e9 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -232,7 +232,6 @@
         "android.hardware.rebootescrow-V1-java",
         "android.hardware.power.stats-V2-java",
         "android.hidl.manager-V1.2-java",
-        "audio-permission-aidl-java",
         "cbor-java",
         "com.android.media.audio-aconfig-java",
         "icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index 966478e..a619257 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -138,12 +138,6 @@
 
     static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
 
-    // Time needed to apply mitigation
-    private static final String MITIGATION_WINDOW_MS =
-            "persist.device_config.configuration.mitigation_window_ms";
-    @VisibleForTesting
-    static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5);
-
     // Threshold level at which or above user might experience significant disruption.
     private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
             "persist.device_config.configuration.major_user_impact_level_threshold";
@@ -216,9 +210,6 @@
     @GuardedBy("mLock")
     private boolean mSyncRequired = false;
 
-    @GuardedBy("mLock")
-    private long mLastMitigation = -1000000;
-
     @FunctionalInterface
     @VisibleForTesting
     interface SystemClock {
@@ -409,16 +400,6 @@
             Slog.w(TAG, "Could not resolve a list of failing packages");
             return;
         }
-        synchronized (mLock) {
-            final long now = mSystemClock.uptimeMillis();
-            if (Flags.recoverabilityDetection()) {
-                if (now >= mLastMitigation
-                        && (now - mLastMitigation) < getMitigationWindowMs()) {
-                    Slog.i(TAG, "Skipping onPackageFailure mitigation");
-                    return;
-                }
-            }
-        }
         mLongTaskHandler.post(() -> {
             synchronized (mLock) {
                 if (mAllObservers.isEmpty()) {
@@ -519,17 +500,10 @@
                               int currentObserverImpact,
                               int mitigationCount) {
         if (currentObserverImpact < getUserImpactLevelLimit()) {
-            synchronized (mLock) {
-                mLastMitigation = mSystemClock.uptimeMillis();
-            }
             currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount);
         }
     }
 
-    private long getMitigationWindowMs() {
-        return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS);
-    }
-
 
     /**
      * Called when the system server boots. If the system server is detected to be in a boot loop,
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index db4840d..211f952 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -83,8 +83,6 @@
 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;
@@ -100,6 +98,8 @@
 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
-                + " and got error " + err);
+                + " to frozen apps 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/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 8647750..ab34dd4 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2205,12 +2205,15 @@
                                     != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0;
 
                     if (roForegroundAudioControl()) { // flag check
-                        final int fgsAudioType = FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
-                                | FOREGROUND_SERVICE_TYPE_CAMERA
-                                | FOREGROUND_SERVICE_TYPE_MICROPHONE
-                                | FOREGROUND_SERVICE_TYPE_PHONE_CALL;
-                        capabilityFromFGS |= (psr.getForegroundServiceTypes() & fgsAudioType) != 0
-                                ? PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL : 0;
+                        // TODO revisit restriction of FOREGROUND_AUDIO_CONTROL when it can be
+                        //      limited to specific FGS types
+                        //final int fgsAudioType = FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+                        //        | FOREGROUND_SERVICE_TYPE_CAMERA
+                        //        | FOREGROUND_SERVICE_TYPE_MICROPHONE
+                        //        | FOREGROUND_SERVICE_TYPE_PHONE_CALL;
+                        //capabilityFromFGS |= (psr.getForegroundServiceTypes() & fgsAudioType) != 0
+                        //        ? PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL : 0;
+                        capabilityFromFGS |= PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL;
                     }
 
                     final boolean enabled = state.getCachedCompatChange(
diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java
index 8d7a1c9..8eef71e 100644
--- a/services/core/java/com/android/server/am/PendingIntentRecord.java
+++ b/services/core/java/com/android/server/am/PendingIntentRecord.java
@@ -22,6 +22,8 @@
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_COMPAT;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SYSTEM_UID;
 
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
 import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -422,6 +424,10 @@
             })
     public static BackgroundStartPrivileges getDefaultBackgroundStartPrivileges(
             int callingUid, @Nullable String callingPackage) {
+        if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) {
+            // root and system must always opt in explicitly
+            return BackgroundStartPrivileges.ALLOW_FGS;
+        }
         boolean isChangeEnabledForApp = callingPackage != null ? CompatChanges.isChangeEnabled(
                 DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,
                 UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 2184340..b9cdf27 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -1005,7 +1005,7 @@
         if (isForeground || foregroundId != 0) {
             pw.print(prefix); pw.print("isForeground="); pw.print(isForeground);
             pw.print(" foregroundId="); pw.print(foregroundId);
-            pw.printf(" types=%08X", foregroundServiceType);
+            pw.printf(" types=0x%08X", foregroundServiceType);
             pw.print(" foregroundNoti="); pw.println(foregroundNoti);
 
             if (isShortFgs() && mShortFgsInfo != null) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 1dc1846..1d21ccb 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.audio;
 
+import static android.media.audio.Flags.scoManagedByAudio;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.compat.CompatChanges;
@@ -54,6 +56,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.sysprop.BluetoothProperties;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -74,7 +77,6 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-
 /**
  * @hide
  * (non final for mocking/spying)
@@ -167,6 +169,15 @@
     @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S_V2)
     public static final long USE_SET_COMMUNICATION_DEVICE = 243827847L;
 
+    /** Indicates if headset profile connection and SCO audio control use the new implementation
+     * aligned with other BT profiles. True if both the feature flag Flags.scoManagedByAudio() and
+     * the system property audio.sco.managed.by.audio are true.
+     */
+    private final boolean mScoManagedByAudio;
+    /*package*/ boolean isScoManagedByAudio() {
+        return mScoManagedByAudio;
+    }
+
     //-------------------------------------------------------------------
     /*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service,
             @NonNull AudioSystemAdapter audioSystem) {
@@ -176,7 +187,8 @@
         mDeviceInventory = new AudioDeviceInventory(this);
         mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
         mAudioSystem = audioSystem;
-
+        mScoManagedByAudio = scoManagedByAudio()
+                && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false);
         init();
     }
 
@@ -192,7 +204,8 @@
         mDeviceInventory = mockDeviceInventory;
         mSystemServer = mockSystemServer;
         mAudioSystem = audioSystem;
-
+        mScoManagedByAudio = scoManagedByAudio()
+                && BluetoothProperties.isScoManagedByAudioEnabled().orElse(false);
         init();
     }
 
@@ -400,24 +413,24 @@
         if (client == null) {
             return;
         }
-
-        boolean isBtScoRequested = isBluetoothScoRequested();
-        if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
-            if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
-                Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
-                        + uid);
-                // clean up or restore previous client selection
-                if (prevClientDevice != null) {
-                    addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged);
-                } else {
-                    removeCommunicationRouteClient(cb, true);
+        if (!mScoManagedByAudio) {
+            boolean isBtScoRequested = isBluetoothScoRequested();
+            if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
+                if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
+                    Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
+                            + uid);
+                    // clean up or restore previous client selection
+                    if (prevClientDevice != null) {
+                        addCommunicationRouteClient(cb, uid, prevClientDevice, prevPrivileged);
+                    } else {
+                        removeCommunicationRouteClient(cb, true);
+                    }
+                    postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
                 }
-                postBroadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
+            } else if (!isBtScoRequested && wasBtScoRequested) {
+                mBtHelper.stopBluetoothSco(eventSource);
             }
-        } else if (!isBtScoRequested && wasBtScoRequested) {
-            mBtHelper.stopBluetoothSco(eventSource);
         }
-
         // In BT classic for communication, the device changes from a2dp to sco device, but for
         // LE Audio it stays the same and we must trigger the proper stream volume alignment, if
         // LE Audio communication device is activated after the audio system has already switched to
@@ -1685,6 +1698,8 @@
 
         pw.println("\n" + prefix + "mAudioModeOwner: " + mAudioModeOwner);
 
+        pw.println("\n" + prefix + "mScoManagedByAudio: " + mScoManagedByAudio);
+
         mBtHelper.dump(pw, prefix);
     }
 
@@ -1837,10 +1852,10 @@
                                             ? mAudioService.getBluetoothContextualVolumeStream()
                                             : AudioSystem.STREAM_DEFAULT);
                                 if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
-                                        || btInfo.mProfile
-                                        == BluetoothProfile.HEARING_AID) {
-                                    onUpdateCommunicationRouteClient(
-                                            isBluetoothScoRequested(),
+                                        || btInfo.mProfile == BluetoothProfile.HEARING_AID
+                                        || (mScoManagedByAudio
+                                            && btInfo.mProfile == BluetoothProfile.HEADSET)) {
+                                    onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
                                             "setBluetoothActiveDevice");
                                 }
                             }
@@ -2511,7 +2526,7 @@
             setCommunicationRouteForClient(crc.getBinder(), crc.getUid(), crc.getDevice(),
                     BtHelper.SCO_MODE_UNDEFINED, crc.isPrivileged(), eventSource);
         } else {
-            if (!isBluetoothScoRequested() && wasBtScoRequested) {
+            if (!mScoManagedByAudio && !isBluetoothScoRequested() && wasBtScoRequested) {
                 mBtHelper.stopBluetoothSco(eventSource);
             }
             updateCommunicationRoute(eventSource);
@@ -2815,4 +2830,5 @@
     void clearDeviceInventory() {
         mDeviceInventory.clearDeviceInventory();
     }
+
 }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e0790da..287c92f 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -859,6 +859,15 @@
                                 btInfo, streamType, codec, "onSetBtActiveDevice");
                     }
                     break;
+                case BluetoothProfile.HEADSET:
+                    if (mDeviceBroker.isScoManagedByAudio()) {
+                        if (switchToUnavailable) {
+                            mDeviceBroker.onSetBtScoActiveDevice(null);
+                        } else if (switchToAvailable) {
+                            mDeviceBroker.onSetBtScoActiveDevice(btInfo.mDevice);
+                        }
+                    }
+                    break;
                 default: throw new IllegalArgumentException("Invalid profile "
                                  + BluetoothProfile.getProfileName(btInfo.mProfile));
             }
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
index 02e80d6..f652b33 100644
--- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
@@ -16,12 +16,14 @@
 
 package com.android.server.audio;
 
+import com.android.media.permission.INativePermissionController;
 
 /**
  * Facade to IAudioPolicyService which fulfills AudioService dependencies.
  * See @link{IAudioPolicyService.aidl}
  */
 public interface AudioPolicyFacade {
-
     public boolean isHotwordStreamSupported(boolean lookbackAudio);
+    public INativePermissionController getPermissionController();
+    public void registerOnStartTask(Runnable r);
 }
diff --git a/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
new file mode 100644
index 0000000..5ea3c4b
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioServerPermissionProvider.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.media.permission.INativePermissionController;
+import com.android.media.permission.UidPackageState;
+import com.android.server.pm.pkg.PackageState;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+
+/** Responsible for synchronizing system server permission state to the native audioserver. */
+public class AudioServerPermissionProvider {
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private INativePermissionController mDest;
+
+    @GuardedBy("mLock")
+    private final Map<Integer, Set<String>> mPackageMap;
+
+    /**
+     * @param appInfos - PackageState for all apps on the device, used to populate init state
+     */
+    public AudioServerPermissionProvider(Collection<PackageState> appInfos) {
+        // Initialize the package state
+        mPackageMap = generatePackageMappings(appInfos);
+    }
+
+    /**
+     * Called whenever audioserver starts (or started before us)
+     *
+     * @param pc - The permission controller interface from audioserver, which we push updates to
+     */
+    public void onServiceStart(@Nullable INativePermissionController pc) {
+        if (pc == null) return;
+        synchronized (mLock) {
+            mDest = pc;
+            resetNativePackageState();
+        }
+    }
+
+    /**
+     * Called when a package is added or removed
+     *
+     * @param uid - uid of modified package (only app-id matters)
+     * @param packageName - the (new) packageName
+     * @param isRemove - true if the package is being removed, false if it is being added
+     */
+    public void onModifyPackageState(int uid, String packageName, boolean isRemove) {
+        // No point in maintaining package mappings for uids of different users
+        uid = UserHandle.getAppId(uid);
+        synchronized (mLock) {
+            // Update state
+            Set<String> packages;
+            if (!isRemove) {
+                packages = mPackageMap.computeIfAbsent(uid, unused -> new ArraySet(1));
+                packages.add(packageName);
+            } else {
+                packages = mPackageMap.get(uid);
+                if (packages != null) {
+                    packages.remove(packageName);
+                    if (packages.isEmpty()) mPackageMap.remove(uid);
+                }
+            }
+            // Push state to destination
+            if (mDest == null) {
+                return;
+            }
+            var state = new UidPackageState();
+            state.uid = uid;
+            state.packageNames = packages != null ? List.copyOf(packages) : Collections.emptyList();
+            try {
+                mDest.updatePackagesForUid(state);
+            } catch (RemoteException e) {
+                // We will re-init the state when the service comes back up
+                mDest = null;
+            }
+        }
+    }
+
+    /** Called when full syncing package state to audioserver. */
+    @GuardedBy("mLock")
+    private void resetNativePackageState() {
+        if (mDest == null) return;
+        List<UidPackageState> states =
+                mPackageMap.entrySet().stream()
+                        .map(
+                                entry -> {
+                                    UidPackageState state = new UidPackageState();
+                                    state.uid = entry.getKey();
+                                    state.packageNames = List.copyOf(entry.getValue());
+                                    return state;
+                                })
+                        .toList();
+        try {
+            mDest.populatePackagesForUids(states);
+        } catch (RemoteException e) {
+            // We will re-init the state when the service comes back up
+            mDest = null;
+        }
+    }
+
+    /**
+     * Aggregation operation on all package states list: groups by states by app-id and merges the
+     * packageName for each state into an ArraySet.
+     */
+    private static Map<Integer, Set<String>> generatePackageMappings(
+            Collection<PackageState> appInfos) {
+        Collector<PackageState, Object, Set<String>> reducer =
+                Collectors.mapping(
+                        (PackageState p) -> p.getPackageName(),
+                        Collectors.toCollection(() -> new ArraySet(1)));
+
+        return appInfos.stream()
+                .collect(
+                        Collectors.groupingBy(
+                                /* predicate */ (PackageState p) -> p.getAppId(),
+                                /* factory */ HashMap::new,
+                                /* downstream collector */ reducer));
+    }
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index cb0ad78..ef65b25 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -31,6 +31,10 @@
 import static android.Manifest.permission.QUERY_AUDIO_STATE;
 import static android.Manifest.permission.WRITE_SETTINGS;
 import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT;
+import static android.content.Intent.ACTION_PACKAGE_ADDED;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
+import static android.content.Intent.EXTRA_ARCHIVAL;
+import static android.content.Intent.EXTRA_REPLACING;
 import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
 import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
@@ -48,6 +52,7 @@
 import static android.media.audio.Flags.featureSpatialAudioHeadtrackingLowLatency;
 import static android.media.audio.Flags.focusFreezeTestApi;
 import static android.media.audio.Flags.roForegroundAudioControl;
+import static android.media.audio.Flags.scoManagedByAudio;
 import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration;
 import static android.os.Process.FIRST_APPLICATION_UID;
 import static android.os.Process.INVALID_UID;
@@ -56,7 +61,9 @@
 import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
+import static com.android.media.audio.Flags.audioserverPermissions;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
 import static com.android.media.audio.Flags.setStreamVolumeOrder;
@@ -238,15 +245,18 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
+import com.android.server.LocalManagerRegistry;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.audio.AudioServiceEvents.DeviceVolumeEvent;
 import com.android.server.audio.AudioServiceEvents.PhoneStateEvent;
 import com.android.server.audio.AudioServiceEvents.VolChangedBroadcastEvent;
 import com.android.server.audio.AudioServiceEvents.VolumeEvent;
+import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
 import com.android.server.pm.UserManagerService;
+import com.android.server.pm.pkg.PackageState;
 import com.android.server.utils.EventLogger;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
@@ -271,6 +281,7 @@
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BooleanSupplier;
@@ -301,6 +312,8 @@
     private final SettingsAdapter mSettings;
     private final AudioPolicyFacade mAudioPolicy;
 
+    private final AudioServerPermissionProvider mPermissionProvider;
+
     private final MusicFxHelper mMusicFxHelper;
 
     /** Debug audio mode */
@@ -631,6 +644,17 @@
     // If absolute volume is supported in AVRCP device
     private volatile boolean mAvrcpAbsVolSupported = false;
 
+    private final Object mCachedAbsVolDrivingStreamsLock = new Object();
+    // Contains for all the device types which support absolute volume the current streams that
+    // are driving the volume changes
+    @GuardedBy("mCachedAbsVolDrivingStreamsLock")
+    private final HashMap<Integer, Integer> mCachedAbsVolDrivingStreams = new HashMap<>(
+            Map.of(AudioSystem.DEVICE_OUT_BLE_HEADSET, AudioSystem.STREAM_MUSIC,
+                    AudioSystem.DEVICE_OUT_BLE_SPEAKER, AudioSystem.STREAM_MUSIC,
+                    AudioSystem.DEVICE_OUT_BLE_BROADCAST, AudioSystem.STREAM_MUSIC,
+                    AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.STREAM_MUSIC
+            ));
+
     /**
     * Default stream type used for volume control in the absence of playback
     * e.g. user on homescreen, no app playing anything, presses hardware volume buttons, this
@@ -1008,14 +1032,22 @@
 
         public Lifecycle(Context context) {
             super(context);
+            var audioserverLifecycleExecutor = Executors.newSingleThreadExecutor();
+            var audioPolicyFacade = new DefaultAudioPolicyFacade(audioserverLifecycleExecutor);
             mService = new AudioService(context,
                               AudioSystemAdapter.getDefaultAdapter(),
                               SystemServerAdapter.getDefaultAdapter(context),
                               SettingsAdapter.getDefaultAdapter(),
                               new AudioVolumeGroupHelper(),
-                              new DefaultAudioPolicyFacade(r -> r.run()),
-                              null);
-
+                              audioPolicyFacade,
+                              null,
+                              context.getSystemService(AppOpsManager.class),
+                              PermissionEnforcer.fromContext(context),
+                              audioserverPermissions() ?
+                                initializeAudioServerPermissionProvider(
+                                    context, audioPolicyFacade, audioserverLifecycleExecutor) :
+                                    null
+                              );
         }
 
         @Override
@@ -1092,25 +1124,6 @@
     /**
      * @param context
      * @param audioSystem Adapter for {@link AudioSystem}
-     * @param systemServer Adapter for privileged functionality for system server components
-     * @param settings Adapter for {@link Settings}
-     * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup}
-     * @param audioPolicy Interface of a facade to IAudioPolicyManager
-     * @param looper Looper to use for the service's message handler. If this is null, an
-     *               {@link AudioSystemThread} is created as the messaging thread instead.
-     */
-    public AudioService(Context context, AudioSystemAdapter audioSystem,
-            SystemServerAdapter systemServer, SettingsAdapter settings,
-            AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
-            @Nullable Looper looper) {
-        this (context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
-                audioPolicy, looper, context.getSystemService(AppOpsManager.class),
-                PermissionEnforcer.fromContext(context));
-    }
-
-    /**
-     * @param context
-     * @param audioSystem Adapter for {@link AudioSystem}
      * @param systemServer Adapter for privilieged functionality for system server components
      * @param settings Adapter for {@link Settings}
      * @param audioVolumeGroupHelper Adapter for {@link AudioVolumeGroup}
@@ -1124,13 +1137,16 @@
     public AudioService(Context context, AudioSystemAdapter audioSystem,
             SystemServerAdapter systemServer, SettingsAdapter settings,
             AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
-            @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer) {
+            @Nullable Looper looper, AppOpsManager appOps, @NonNull PermissionEnforcer enforcer,
+            /* @NonNull */ AudioServerPermissionProvider permissionProvider) {
         super(enforcer);
         sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
         mContext = context;
         mContentResolver = context.getContentResolver();
         mAppOps = appOps;
 
+        mPermissionProvider = permissionProvider;
+
         mAudioSystem = audioSystem;
         mSystemServer = systemServer;
         mAudioVolumeGroupHelper = audioVolumeGroupHelper;
@@ -1471,6 +1487,13 @@
 
         // check on volume initialization
         checkVolumeRangeInitialization("AudioService()");
+
+        synchronized (mCachedAbsVolDrivingStreamsLock) {
+            mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
+                mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
+                        stream);
+            });
+        }
     }
 
     private SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionChangedListener =
@@ -1503,7 +1526,9 @@
         // Register for device connection intent broadcasts.
         IntentFilter intentFilter =
                 new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
-        intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
+        if (!mDeviceBroker.isScoManagedByAudio()) {
+            intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
+        }
         intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
         if (mDisplayManager == null) {
             intentFilter.addAction(Intent.ACTION_SCREEN_ON);
@@ -1908,6 +1933,14 @@
         }
 
         onIndicateSystemReady();
+
+        synchronized (mCachedAbsVolDrivingStreamsLock) {
+            mCachedAbsVolDrivingStreams.forEach((dev, stream) -> {
+                mAudioSystem.setDeviceAbsoluteVolumeEnabled(dev, /*address=*/"", /*enabled=*/true,
+                        stream);
+            });
+        }
+
         // indicate the end of reconfiguration phase to audio HAL
         AudioSystem.setParameters("restarting=false");
 
@@ -3734,8 +3767,10 @@
 
             int newIndex = mStreamStates[streamType].getIndex(device);
 
+            int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() :
+                    AudioSystem.STREAM_MUSIC;
             // Check if volume update should be send to AVRCP
-            if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+            if (streamTypeAlias == streamToDriveAbsVol
                     && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                     && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
                 if (DEBUG_VOL) {
@@ -4527,15 +4562,20 @@
                 + featureSpatialAudioHeadtrackingLowLatency());
         pw.println("\tandroid.media.audio.focusFreezeTestApi:"
                 + focusFreezeTestApi());
+        pw.println("\tcom.android.media.audio.audioserverPermissions:"
+                + audioserverPermissions());
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
-
         pw.println("\tcom.android.media.audio.setStreamVolumeOrder:"
                 + setStreamVolumeOrder());
         pw.println("\tandroid.media.audio.roForegroundAudioControl:"
                 + roForegroundAudioControl());
+        pw.println("\tandroid.media.audio.scoManagedByAudio:"
+                + scoManagedByAudio());
         pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
                 + vgsVssSyncMuteOrder());
+        pw.println("\tcom.android.media.audio.absVolumeIndexFix:"
+                + absVolumeIndexFix());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -4731,7 +4771,9 @@
             }
         }
 
-        if (streamTypeAlias == AudioSystem.STREAM_MUSIC
+        int streamToDriveAbsVol = absVolumeIndexFix() ? getBluetoothContextualVolumeStream() :
+                AudioSystem.STREAM_MUSIC;
+        if (streamTypeAlias == streamToDriveAbsVol
                 && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
             if (DEBUG_VOL) {
@@ -6180,6 +6222,17 @@
 
                 setLeAudioVolumeOnModeUpdate(mode, device, streamAlias, index, maxIndex);
 
+                synchronized (mCachedAbsVolDrivingStreamsLock) {
+                    mCachedAbsVolDrivingStreams.replaceAll((absDev, stream) -> {
+                        int streamToDriveAbs = getBluetoothContextualVolumeStream();
+                        if (stream != streamToDriveAbs) {
+                            mAudioSystem.setDeviceAbsoluteVolumeEnabled(absDev, /*address=*/
+                                    "", /*enabled*/true, streamToDriveAbs);
+                        }
+                        return streamToDriveAbs;
+                    });
+                }
+
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
                 mDeviceBroker.postSetModeOwner(mode, pid, uid);
@@ -7859,7 +7912,8 @@
         if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK
                 && profile != BluetoothProfile.LE_AUDIO
                 && profile != BluetoothProfile.LE_AUDIO_BROADCAST
-                && profile != BluetoothProfile.HEARING_AID) {
+                && profile != BluetoothProfile.HEARING_AID
+                && !(mDeviceBroker.isScoManagedByAudio() && profile == BluetoothProfile.HEADSET)) {
             throw new IllegalArgumentException("Illegal BluetoothProfile profile for device "
                     + previousDevice + " -> " + newDevice + ". Got: " + profile);
         }
@@ -8100,6 +8154,10 @@
             return mAudioVolumeGroup.name();
         }
 
+        public int getId() {
+            return mAudioVolumeGroup.getId();
+        }
+
         /**
          * Volume group with non null minimum index are considered as non mutable, thus
          * bijectivity is broken with potential associated stream type.
@@ -8750,24 +8808,30 @@
         }
 
         private int getAbsoluteVolumeIndex(int index) {
-            /* Special handling for Bluetooth Absolute Volume scenario
-             * If we send full audio gain, some accessories are too loud even at its lowest
-             * volume. We are not able to enumerate all such accessories, so here is the
-             * workaround from phone side.
-             * Pre-scale volume at lowest volume steps 1 2 and 3.
-             * For volume step 0, set audio gain to 0 as some accessories won't mute on their end.
-             */
-            if (index == 0) {
-                // 0% for volume 0
-                index = 0;
-            } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) {
-                // Pre-scale for volume steps 1 2 and 3
-                index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
+            if (absVolumeIndexFix()) {
+                // The attenuation is applied in the APM. No need to manipulate the index here
+                return index;
             } else {
-                // otherwise, full gain
-                index = (mIndexMax + 5) / 10;
+                /* Special handling for Bluetooth Absolute Volume scenario
+                 * If we send full audio gain, some accessories are too loud even at its lowest
+                 * volume. We are not able to enumerate all such accessories, so here is the
+                 * workaround from phone side.
+                 * Pre-scale volume at lowest volume steps 1 2 and 3.
+                 * For volume step 0, set audio gain to 0 as some accessories won't mute on their
+                 * end.
+                 */
+                if (index == 0) {
+                    // 0% for volume 0
+                    index = 0;
+                } else if (!disablePrescaleAbsoluteVolume() && index > 0 && index <= 3) {
+                    // Pre-scale for volume steps 1 2 and 3
+                    index = (int) (mIndexMax * mPrescaleAbsoluteVolume[index - 1]) / 10;
+                } else {
+                    // otherwise, full gain
+                    index = (mIndexMax + 5) / 10;
+                }
+                return index;
             }
-            return index;
         }
 
         private void setStreamVolumeIndex(int index, int device) {
@@ -8778,6 +8842,11 @@
                     && !isFullyMuted()) {
                 index = 1;
             }
+
+            if (DEBUG_VOL) {
+                Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device
+                        + ")");
+            }
             mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
         }
 
@@ -8789,14 +8858,24 @@
             } else if (isAbsoluteVolumeDevice(device)
                     || isA2dpAbsoluteVolumeDevice(device)
                     || AudioSystem.isLeAudioDeviceType(device)) {
-                index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+                // do not change the volume logic for dynamic abs behavior devices like HDMI
+                if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) {
+                    index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10);
+                } else {
+                    index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+                }
             } else if (isFullVolumeDevice(device)) {
                 index = (mIndexMax + 5)/10;
             } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
-                index = (mIndexMax + 5)/10;
+                if (absVolumeIndexFix()) {
+                    index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+                } else {
+                    index = (mIndexMax + 5) / 10;
+                }
             } else {
                 index = (getIndex(device) + 5)/10;
             }
+
             setStreamVolumeIndex(index, device);
         }
 
@@ -8814,11 +8893,22 @@
                                 || isA2dpAbsoluteVolumeDevice(device)
                                 || AudioSystem.isLeAudioDeviceType(device)) {
                             isAbsoluteVolume = true;
-                            index = getAbsoluteVolumeIndex((getIndex(device) + 5)/10);
+                            // do not change the volume logic for dynamic abs behavior devices
+                            // like HDMI
+                            if (absVolumeIndexFix() && isAbsoluteVolumeDevice(device)) {
+                                index = getAbsoluteVolumeIndex((mIndexMax + 5) / 10);
+                            } else {
+                                index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+                            }
                         } else if (isFullVolumeDevice(device)) {
                             index = (mIndexMax + 5)/10;
                         } else if (device == AudioSystem.DEVICE_OUT_HEARING_AID) {
-                            index = (mIndexMax + 5)/10;
+                            if (absVolumeIndexFix()) {
+                                isAbsoluteVolume = true;
+                                index = getAbsoluteVolumeIndex((getIndex(device) + 5) / 10);
+                            } else {
+                                index = (mIndexMax + 5) / 10;
+                            }
                         } else {
                             index = (mIndexMap.valueAt(i) + 5)/10;
                         }
@@ -9815,6 +9905,27 @@
 
     /*package*/ void setAvrcpAbsoluteVolumeSupported(boolean support) {
         mAvrcpAbsVolSupported = support;
+        if (absVolumeIndexFix()) {
+            int a2dpDev = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP;
+            synchronized (mCachedAbsVolDrivingStreamsLock) {
+                mCachedAbsVolDrivingStreams.compute(a2dpDev, (dev, stream) -> {
+                    if (stream != null && !mAvrcpAbsVolSupported) {
+                        mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
+                                "", /*enabled*/false, AudioSystem.DEVICE_NONE);
+                        return null;
+                    }
+                    // For A2DP and AVRCP we need to set the driving stream based on the
+                    // BT contextual stream. Hence, we need to make sure in adjustStreamVolume
+                    // and setStreamVolume that the driving abs volume stream is consistent.
+                    int streamToDriveAbs = getBluetoothContextualVolumeStream();
+                    if (stream == null || stream != streamToDriveAbs) {
+                        mAudioSystem.setDeviceAbsoluteVolumeEnabled(a2dpDev, /*address=*/
+                                "", /*enabled*/true, streamToDriveAbs);
+                    }
+                    return streamToDriveAbs;
+                });
+            }
+        }
         sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE,
                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, 0,
                     mStreamStates[AudioSystem.STREAM_MUSIC], 0);
@@ -11831,6 +11942,45 @@
     private static final String mMetricsId = MediaMetrics.Name.AUDIO_SERVICE
             + MediaMetrics.SEPARATOR;
 
+    private static AudioServerPermissionProvider initializeAudioServerPermissionProvider(
+            Context context, AudioPolicyFacade audioPolicy, Executor audioserverExecutor) {
+        Collection<PackageState> packageStates = null;
+        try (PackageManagerLocal.UnfilteredSnapshot snapshot =
+                    LocalManagerRegistry.getManager(PackageManagerLocal.class)
+                        .withUnfilteredSnapshot()) {
+            packageStates = snapshot.getPackageStates().values();
+        }
+        var provider = new AudioServerPermissionProvider(packageStates);
+        audioPolicy.registerOnStartTask(() -> {
+            provider.onServiceStart(audioPolicy.getPermissionController());
+        });
+
+        // Set up event listeners
+        IntentFilter packageUpdateFilter = new IntentFilter();
+        packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED);
+        packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED);
+        packageUpdateFilter.addDataScheme("package");
+
+        context.registerReceiverForAllUsers(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                String action = intent.getAction();
+                String pkgName = intent.getData().getEncodedSchemeSpecificPart();
+                int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
+                if (intent.getBooleanExtra(EXTRA_REPLACING, false) ||
+                        intent.getBooleanExtra(EXTRA_ARCHIVAL, false)) return;
+                if (action.equals(ACTION_PACKAGE_ADDED)) {
+                    audioserverExecutor.execute(() ->
+                            provider.onModifyPackageState(uid, pkgName, false /* isRemoved */));
+                } else if (action.equals(ACTION_PACKAGE_REMOVED)) {
+                    audioserverExecutor.execute(() ->
+                            provider.onModifyPackageState(uid, pkgName, true /* isRemoved */));
+                }
+            }
+        }, packageUpdateFilter, null, null); // main thread is fine, since dispatch on executor
+        return provider;
+    }
+
     // Inform AudioFlinger of our device's low RAM attribute
     private static void readAndSetLowRamDevice()
     {
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 7202fa2..7f4bc74 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -598,6 +598,21 @@
     }
 
     /**
+     * Same as {@link AudioSystem#setDeviceAbsoluteVolumeEnabled(int, String, boolean, int)}
+     * @param nativeDeviceType the internal device type for which absolute volume is
+     *                         enabled/disabled
+     * @param address the address of the device for which absolute volume is enabled/disabled
+     * @param enabled whether the absolute volume is enabled/disabled
+     * @param streamToDriveAbs the stream that is controlling the absolute volume
+     * @return status of indicating the success of this operation
+     */
+    public int setDeviceAbsoluteVolumeEnabled(int nativeDeviceType, @NonNull String address,
+            boolean enabled, int streamToDriveAbs) {
+        return AudioSystem.setDeviceAbsoluteVolumeEnabled(nativeDeviceType, address, enabled,
+                streamToDriveAbs);
+    }
+
+    /**
      * Same as {@link AudioSystem#registerPolicyMixes(ArrayList, boolean)}
      * @param mixes
      * @param register
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index b1ea85c..6bb3eb1 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -401,50 +401,67 @@
     private void onScoAudioStateChanged(int state) {
         boolean broadcast = false;
         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
-        switch (state) {
-            case BluetoothHeadset.STATE_AUDIO_CONNECTED:
-                scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
-                if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
-                        && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
-                    mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
-                } else if (mDeviceBroker.isBluetoothScoRequested()) {
-                    // broadcast intent if the connection was initated by AudioService
+        if (mDeviceBroker.isScoManagedByAudio()) {
+            switch (state) {
+                case BluetoothHeadset.STATE_AUDIO_CONNECTED:
+                    mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
                     broadcast = true;
-                }
-                mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
-                break;
-            case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
-                mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
-                scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
-                // There are two cases where we want to immediately reconnect audio:
-                // 1) If a new start request was received while disconnecting: this was
-                // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ.
-                // 2) If audio was connected then disconnected via Bluetooth APIs and
-                // we still have pending activation requests by apps: this is indicated by
-                // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested.
-                if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
-                    if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
-                            && connectBluetoothScoAudioHelper(mBluetoothHeadset,
-                            mBluetoothHeadsetDevice, mScoAudioMode)) {
-                        mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
-                        scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING;
+                    break;
+                case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+                    mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+                    broadcast = true;
+                    break;
+                default:
+                    break;
+            }
+        } else {
+            switch (state) {
+                case BluetoothHeadset.STATE_AUDIO_CONNECTED:
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
+                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                    } else if (mDeviceBroker.isBluetoothScoRequested()) {
+                        // broadcast intent if the connection was initated by AudioService
                         broadcast = true;
-                        break;
                     }
-                }
-                if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) {
-                    broadcast = true;
-                }
-                mScoAudioState = SCO_STATE_INACTIVE;
-                break;
-            case BluetoothHeadset.STATE_AUDIO_CONNECTING:
-                if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
-                        && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
-                    mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
-                }
-                break;
-            default:
-                break;
+                    mDeviceBroker.setBluetoothScoOn(true, "BtHelper.onScoAudioStateChanged");
+                    break;
+                case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
+                    mDeviceBroker.setBluetoothScoOn(false, "BtHelper.onScoAudioStateChanged");
+                    scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
+                    // There are two cases where we want to immediately reconnect audio:
+                    // 1) If a new start request was received while disconnecting: this was
+                    // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ.
+                    // 2) If audio was connected then disconnected via Bluetooth APIs and
+                    // we still have pending activation requests by apps: this is indicated by
+                    // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested.
+                    if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
+                        if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
+                                && connectBluetoothScoAudioHelper(mBluetoothHeadset,
+                                mBluetoothHeadsetDevice, mScoAudioMode)) {
+                            mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
+                            scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING;
+                            broadcast = true;
+                            break;
+                        }
+                    }
+                    if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) {
+                        broadcast = true;
+                    }
+                    mScoAudioState = SCO_STATE_INACTIVE;
+                    break;
+                case BluetoothHeadset.STATE_AUDIO_CONNECTING:
+                    if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
+                            && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
+                        mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
+                    }
+                    break;
+                default:
+                    break;
+            }
         }
         if (broadcast) {
             broadcastScoConnectionState(scoAudioState);
@@ -454,7 +471,6 @@
             newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);
             sendStickyBroadcastToAll(newIntent);
         }
-
     }
     /**
      *
diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
index 75febbc..09701e4 100644
--- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
@@ -16,10 +16,15 @@
 
 package com.android.server.audio;
 
+import android.annotation.Nullable;
 import android.media.IAudioPolicyService;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 
+import com.android.media.permission.INativePermissionController;
+
+import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Function;
 
@@ -43,6 +48,7 @@
                         (Function<IBinder, IAudioPolicyService>)
                                 IAudioPolicyService.Stub::asInterface,
                         e);
+        mServiceHolder.registerOnStartTask(i -> Binder.allowBlocking(i.asBinder()));
     }
 
     @Override
@@ -55,4 +61,23 @@
             throw new IllegalStateException();
         }
     }
+
+    @Override
+    public @Nullable INativePermissionController getPermissionController() {
+        IAudioPolicyService ap = mServiceHolder.checkService();
+        if (ap == null) return null;
+        try {
+            var res = Objects.requireNonNull(ap.getPermissionController());
+            Binder.allowBlocking(res.asBinder());
+            return res;
+        } catch (RemoteException e) {
+            mServiceHolder.attemptClear(ap.asBinder());
+            return null;
+        }
+    }
+
+    @Override
+    public void registerOnStartTask(Runnable task) {
+        mServiceHolder.registerOnStartTask(unused -> task.run());
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/biometrics.aconfig b/services/core/java/com/android/server/biometrics/biometrics.aconfig
index 712dcee..92fd9cb 100644
--- a/services/core/java/com/android/server/biometrics/biometrics.aconfig
+++ b/services/core/java/com/android/server/biometrics/biometrics.aconfig
@@ -14,10 +14,3 @@
   description: "This flag controls whether virtual HAL is used for testing instead of TestHal "
   bug: "294254230"
 }
-
-flag {
-  name: "mandatory_biometrics"
-  namespace: "biometrics_framework"
-  description: "This flag controls whether LSKF fallback is removed from biometric prompt when the phone is outside trusted locations"
-  bug: "322081563"
-}
diff --git a/services/core/java/com/android/server/display/DisplayAdapter.java b/services/core/java/com/android/server/display/DisplayAdapter.java
index 5690a9e..69bc66f 100644
--- a/services/core/java/com/android/server/display/DisplayAdapter.java
+++ b/services/core/java/com/android/server/display/DisplayAdapter.java
@@ -135,7 +135,7 @@
             float[] alternativeRefreshRates,
             @Display.HdrCapabilities.HdrType int[] supportedHdrTypes) {
         return new Display.Mode(NEXT_DISPLAY_MODE_ID.getAndIncrement(), width, height, refreshRate,
-                vsyncRate, false, alternativeRefreshRates, supportedHdrTypes);
+                vsyncRate, /* isSynthetic= */ false, alternativeRefreshRates, supportedHdrTypes);
     }
 
     public interface Listener {
diff --git a/services/core/java/com/android/server/display/DisplayBrightnessState.java b/services/core/java/com/android/server/display/DisplayBrightnessState.java
index baa154d..184ae41 100644
--- a/services/core/java/com/android/server/display/DisplayBrightnessState.java
+++ b/services/core/java/com/android/server/display/DisplayBrightnessState.java
@@ -47,6 +47,8 @@
     private final BrightnessEvent mBrightnessEvent;
     private final int mBrightnessAdjustmentFlag;
 
+    private final boolean mIsUserInitiatedChange;
+
     private DisplayBrightnessState(Builder builder) {
         mBrightness = builder.getBrightness();
         mSdrBrightness = builder.getSdrBrightness();
@@ -60,6 +62,7 @@
         mShouldUpdateScreenBrightnessSetting = builder.shouldUpdateScreenBrightnessSetting();
         mBrightnessEvent = builder.getBrightnessEvent();
         mBrightnessAdjustmentFlag = builder.getBrightnessAdjustmentFlag();
+        mIsUserInitiatedChange = builder.isUserInitiatedChange();
     }
 
     /**
@@ -148,6 +151,13 @@
         return mBrightnessAdjustmentFlag;
     }
 
+    /**
+     * Gets if the current brightness changes are because of a user initiated change
+     */
+    public boolean isUserInitiatedChange() {
+        return mIsUserInitiatedChange;
+    }
+
     @Override
     public String toString() {
         StringBuilder stringBuilder = new StringBuilder("DisplayBrightnessState:");
@@ -168,6 +178,7 @@
         stringBuilder.append("\n    mBrightnessEvent:")
                 .append(Objects.toString(mBrightnessEvent, "null"));
         stringBuilder.append("\n    mBrightnessAdjustmentFlag:").append(mBrightnessAdjustmentFlag);
+        stringBuilder.append("\n    mIsUserInitiatedChange:").append(mIsUserInitiatedChange);
         return stringBuilder.toString();
     }
 
@@ -199,7 +210,8 @@
                 && mShouldUpdateScreenBrightnessSetting
                     == otherState.shouldUpdateScreenBrightnessSetting()
                 && Objects.equals(mBrightnessEvent, otherState.getBrightnessEvent())
-                && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag();
+                && mBrightnessAdjustmentFlag == otherState.getBrightnessAdjustmentFlag()
+                && mIsUserInitiatedChange == otherState.isUserInitiatedChange();
     }
 
     @Override
@@ -207,7 +219,8 @@
         return Objects.hash(mBrightness, mSdrBrightness, mBrightnessReason,
                 mShouldUseAutoBrightness, mIsSlowChange, mMaxBrightness, mMinBrightness,
                 mCustomAnimationRate,
-                mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag);
+                mShouldUpdateScreenBrightnessSetting, mBrightnessEvent, mBrightnessAdjustmentFlag,
+                mIsUserInitiatedChange);
     }
 
     /**
@@ -236,6 +249,8 @@
 
         public int mBrightnessAdjustmentFlag = 0;
 
+        private boolean mIsUserInitiatedChange;
+
         /**
          * Create a builder starting with the values from the specified {@link
          * DisplayBrightnessState}.
@@ -257,6 +272,7 @@
                     state.shouldUpdateScreenBrightnessSetting());
             builder.setBrightnessEvent(state.getBrightnessEvent());
             builder.setBrightnessAdjustmentFlag(state.getBrightnessAdjustmentFlag());
+            builder.setIsUserInitiatedChange(state.isUserInitiatedChange());
             return builder;
         }
 
@@ -464,5 +480,20 @@
             mBrightnessAdjustmentFlag = brightnessAdjustmentFlag;
             return this;
         }
+
+        /**
+         * Gets if the current change is a user initiated change
+         */
+        public boolean isUserInitiatedChange() {
+            return mIsUserInitiatedChange;
+        }
+
+        /**
+         * This is used to set if the current change is a user initiated change
+         */
+        public Builder setIsUserInitiatedChange(boolean isUserInitiatedChange) {
+            mIsUserInitiatedChange = isUserInitiatedChange;
+            return this;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index d4c0b01..5fd0253 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -49,6 +49,8 @@
 import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
 import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
 
+import static com.android.server.display.layout.Layout.Display.POSITION_REAR;
+
 import android.Manifest;
 import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
@@ -4972,8 +4974,9 @@
                 }
 
                 final DisplayDevice displayDevice = display.getPrimaryDisplayDeviceLocked();
-                final boolean ownContent = (displayDevice.getDisplayDeviceInfoLocked().flags
-                        & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) != 0;
+                final boolean isRearDisplay = display.getDevicePositionLocked() == POSITION_REAR;
+                final boolean ownContent = ((displayDevice.getDisplayDeviceInfoLocked().flags
+                        & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) != 0) || isRearDisplay;
                 // If the display has enabled mirroring, but specified that it will be managed by
                 // WindowManager, return an invalid display id. This is to ensure we don't
                 // accidentally select the display id to mirror based on DM logic and instead allow
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index b97053b..8d71c70 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1391,6 +1391,7 @@
         // request changes.
         final boolean wasShortTermModelActive =
                 mAutomaticBrightnessStrategy.isShortTermModelActive();
+        boolean userInitiatedChange = displayBrightnessState.isUserInitiatedChange();
         boolean allowAutoBrightnessWhileDozing =
                 mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig();
         if (mFlags.offloadControlsDozeAutoBrightness() && mFlags.isDisplayOffloadEnabled()
@@ -1413,13 +1414,14 @@
                     mPowerRequest.policy,
                     mDisplayBrightnessController.getLastUserSetScreenBrightness(),
                     userSetBrightnessChanged);
+
+            // If the brightness is already set then it's been overridden by something other than
+            // the user, or is a temporary adjustment.
+            userInitiatedChange = (Float.isNaN(brightnessState))
+                    && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
+                    || userSetBrightnessChanged);
         }
 
-        // If the brightness is already set then it's been overridden by something other than the
-        // user, or is a temporary adjustment.
-        boolean userInitiatedChange = (Float.isNaN(brightnessState))
-                && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()
-                || userSetBrightnessChanged);
 
         final int autoBrightnessState = mAutomaticBrightnessStrategy.isAutoBrightnessEnabled()
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 182b05a..44846f3 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -168,6 +168,12 @@
             }
             SurfaceControl.DesiredDisplayModeSpecs modeSpecs =
                     mSurfaceControlProxy.getDesiredDisplayModeSpecs(displayToken);
+            if (modeSpecs == null) {
+                // If mode specs is null, it most probably means that display got
+                // unplugged very rapidly.
+                Slog.w(TAG, "Desired display mode specs from SurfaceFlinger are null");
+                return;
+            }
             LocalDisplayDevice device = mDevices.get(physicalDisplayId);
             if (device == null) {
                 // Display was added.
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index aa17df6..d567331 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -600,6 +600,7 @@
     private StrategyExecutionRequest constructStrategyExecutionRequest(
             DisplayManagerInternal.DisplayPowerRequest displayPowerRequest) {
         float currentScreenBrightness = getCurrentBrightness();
-        return new StrategyExecutionRequest(displayPowerRequest, currentScreenBrightness);
+        return new StrategyExecutionRequest(displayPowerRequest, currentScreenBrightness,
+                mUserSetScreenBrightnessUpdated);
     }
 }
diff --git a/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java b/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java
index 82c0bbf..304640b 100644
--- a/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java
+++ b/services/core/java/com/android/server/display/brightness/StrategyExecutionRequest.java
@@ -29,10 +29,14 @@
 
     private final float mCurrentScreenBrightness;
 
+    // Represents if the user set screen brightness was changed or not.
+    private boolean mUserSetBrightnessChanged;
+
     public StrategyExecutionRequest(DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
-            float currentScreenBrightness) {
+            float currentScreenBrightness, boolean userSetBrightnessChanged) {
         mDisplayPowerRequest = displayPowerRequest;
         mCurrentScreenBrightness = currentScreenBrightness;
+        mUserSetBrightnessChanged = userSetBrightnessChanged;
     }
 
     public DisplayManagerInternal.DisplayPowerRequest getDisplayPowerRequest() {
@@ -43,6 +47,10 @@
         return mCurrentScreenBrightness;
     }
 
+    public boolean isUserSetBrightnessChanged() {
+        return mUserSetBrightnessChanged;
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (!(obj instanceof StrategyExecutionRequest)) {
@@ -50,11 +58,13 @@
         }
         StrategyExecutionRequest other = (StrategyExecutionRequest) obj;
         return Objects.equals(mDisplayPowerRequest, other.getDisplayPowerRequest())
-                && mCurrentScreenBrightness == other.getCurrentScreenBrightness();
+                && mCurrentScreenBrightness == other.getCurrentScreenBrightness()
+                && mUserSetBrightnessChanged == other.isUserSetBrightnessChanged();
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mDisplayPowerRequest, mCurrentScreenBrightness);
+        return Objects.hash(mDisplayPowerRequest, mCurrentScreenBrightness,
+                mUserSetBrightnessChanged);
     }
 }
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 2907364..2b5241f 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
@@ -290,6 +290,8 @@
                 .setBrightnessAdjustmentFlag(mAutoBrightnessAdjustmentReasonsFlags)
                 .setShouldUpdateScreenBrightnessSetting(
                         brightness != strategyExecutionRequest.getCurrentScreenBrightness())
+                .setIsUserInitiatedChange(getAutoBrightnessAdjustmentChanged()
+                        || strategyExecutionRequest.isUserSetBrightnessChanged())
                 .build();
     }
 
diff --git a/services/core/java/com/android/server/display/brightness/strategy/FallbackBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/FallbackBrightnessStrategy.java
index 3463649a..0b92317 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/FallbackBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/FallbackBrightnessStrategy.java
@@ -45,6 +45,7 @@
                 // The fallback brightness might change due to clamping. Make sure we tell the rest
                 // of the system by updating the setting
                 .setShouldUpdateScreenBrightnessSetting(true)
+                .setIsUserInitiatedChange(strategyExecutionRequest.isUserSetBrightnessChanged())
                 .build();
     }
 
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 f923cbc9..f56d803 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -353,7 +353,7 @@
     }
 
     /**
-     * @return Whether to ignore preferredRefreshRate app request or not
+     * @return Whether to ignore preferredRefreshRate app request conversion to display mode or not
      */
     public boolean ignoreAppPreferredRefreshRateRequest() {
         return mIgnoreAppPreferredRefreshRate.isEnabled();
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 76a2827..d519748 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -1254,13 +1254,9 @@
      *  Responsible for keeping track of app requested refresh rates per display
      */
     public final class AppRequestObserver {
-        private final SparseArray<Display.Mode> mAppRequestedModeByDisplay;
-        private final SparseArray<RefreshRateRange> mAppPreferredRefreshRateRangeByDisplay;
         private final boolean mIgnorePreferredRefreshRate;
 
         AppRequestObserver(DisplayManagerFlags flags) {
-            mAppRequestedModeByDisplay = new SparseArray<>();
-            mAppPreferredRefreshRateRangeByDisplay = new SparseArray<>();
             mIgnorePreferredRefreshRate = flags.ignoreAppPreferredRefreshRateRequest();
         }
 
@@ -1269,34 +1265,77 @@
          */
         public void setAppRequest(int displayId, int modeId, float requestedRefreshRate,
                 float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) {
+            Display.Mode requestedMode;
+            synchronized (mLock) {
+                requestedMode = findModeLocked(displayId, modeId, requestedRefreshRate);
+            }
 
-            if (modeId == 0 && requestedRefreshRate != 0 && !mIgnorePreferredRefreshRate) {
+            Vote frameRateVote = getFrameRateVote(
+                    requestedMinRefreshRateRange, requestedMaxRefreshRateRange);
+            Vote baseModeRefreshRateVote = getBaseModeVote(requestedMode, requestedRefreshRate);
+            Vote sizeVote = getSizeVote(requestedMode);
+
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                    frameRateVote);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                    baseModeRefreshRateVote);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+        }
+
+        private Display.Mode findModeLocked(int displayId, int modeId, float requestedRefreshRate) {
+            Display.Mode mode = null;
+            if (modeId != 0) {
+                mode = findAppModeByIdLocked(displayId, modeId);
+            } else if (requestedRefreshRate != 0 && !mIgnorePreferredRefreshRate) { // modeId == 0
                 // Scan supported modes returned to find a mode with the same
                 // size as the default display mode but with the specified refresh rate instead.
-                Display.Mode mode = findDefaultModeByRefreshRate(displayId, requestedRefreshRate);
-                if (mode != null) {
-                    modeId = mode.getModeId();
-                } else {
+                mode = findDefaultModeByRefreshRateLocked(displayId, requestedRefreshRate);
+                if (mode == null) {
                     Slog.e(TAG, "Couldn't find a mode for the requestedRefreshRate: "
                             + requestedRefreshRate + " on Display: " + displayId);
                 }
             }
+            return mode;
+        }
 
-            synchronized (mLock) {
-                setAppRequestedModeLocked(displayId, modeId);
-                setAppPreferredRefreshRateRangeLocked(displayId, requestedMinRefreshRateRange,
-                        requestedMaxRefreshRateRange);
+        private Vote getFrameRateVote(float minRefreshRate, float maxRefreshRate) {
+            RefreshRateRange refreshRateRange = null;
+            if (minRefreshRate > 0 || maxRefreshRate > 0) {
+                float max = maxRefreshRate > 0
+                        ? maxRefreshRate : Float.POSITIVE_INFINITY;
+                refreshRateRange = new RefreshRateRange(minRefreshRate, max);
+                if (refreshRateRange.min == 0 && refreshRateRange.max == 0) {
+                    // minRefreshRate/maxRefreshRate were invalid
+                    refreshRateRange = null;
+                }
             }
+            return refreshRateRange != null
+                    ? Vote.forRenderFrameRates(refreshRateRange.min, refreshRateRange.max) : null;
+        }
+
+        private Vote getSizeVote(@Nullable Display.Mode mode) {
+            return mode != null
+                    ?  Vote.forSize(mode.getPhysicalWidth(), mode.getPhysicalHeight()) : null;
+        }
+
+        private Vote getBaseModeVote(@Nullable Display.Mode mode, float requestedRefreshRate) {
+            Vote vote = null;
+            if (mode != null) {
+                if (mode.isSynthetic()) {
+                    vote = Vote.forRequestedRefreshRate(mode.getRefreshRate());
+                } else {
+                    vote = Vote.forBaseModeRefreshRate(mode.getRefreshRate());
+                }
+            } else if (requestedRefreshRate != 0f && mIgnorePreferredRefreshRate) {
+                vote = Vote.forRequestedRefreshRate(requestedRefreshRate);
+            } // !mIgnorePreferredRefreshRate case is handled by findModeLocked
+            return vote;
         }
 
         @Nullable
-        private Display.Mode findDefaultModeByRefreshRate(int displayId, float refreshRate) {
-            Display.Mode[] modes;
-            Display.Mode defaultMode;
-            synchronized (mLock) {
-                modes = mAppSupportedModesByDisplay.get(displayId);
-                defaultMode = mDefaultModeByDisplay.get(displayId);
-            }
+        private Display.Mode findDefaultModeByRefreshRateLocked(int displayId, float refreshRate) {
+            Display.Mode[] modes = mAppSupportedModesByDisplay.get(displayId);
+            Display.Mode defaultMode = mDefaultModeByDisplay.get(displayId);
             for (int i = 0; i < modes.length; i++) {
                 if (modes[i].matches(defaultMode.getPhysicalWidth(),
                         defaultMode.getPhysicalHeight(), refreshRate)) {
@@ -1306,69 +1345,6 @@
             return null;
         }
 
-        private void setAppRequestedModeLocked(int displayId, int modeId) {
-            final Display.Mode requestedMode = findAppModeByIdLocked(displayId, modeId);
-            if (Objects.equals(requestedMode, mAppRequestedModeByDisplay.get(displayId))) {
-                return;
-            }
-            final Vote baseModeRefreshRateVote;
-            final Vote sizeVote;
-            if (requestedMode != null) {
-                mAppRequestedModeByDisplay.put(displayId, requestedMode);
-                sizeVote = Vote.forSize(requestedMode.getPhysicalWidth(),
-                        requestedMode.getPhysicalHeight());
-                if (requestedMode.isSynthetic()) {
-                    // TODO: for synthetic mode we should not limit frame rate, we must ensure
-                    // that frame rate is reachable within other Votes constraints
-                    baseModeRefreshRateVote = Vote.forRenderFrameRates(
-                            requestedMode.getRefreshRate(), requestedMode.getRefreshRate());
-                } else {
-                    baseModeRefreshRateVote =
-                            Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
-                }
-            } else {
-                mAppRequestedModeByDisplay.remove(displayId);
-                baseModeRefreshRateVote = null;
-                sizeVote = null;
-            }
-
-            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
-                    baseModeRefreshRateVote);
-            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
-        }
-
-        private void setAppPreferredRefreshRateRangeLocked(int displayId,
-                float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) {
-            final Vote vote;
-
-            RefreshRateRange refreshRateRange = null;
-            if (requestedMinRefreshRateRange > 0 || requestedMaxRefreshRateRange > 0) {
-                float min = requestedMinRefreshRateRange;
-                float max = requestedMaxRefreshRateRange > 0
-                        ? requestedMaxRefreshRateRange : Float.POSITIVE_INFINITY;
-                refreshRateRange = new RefreshRateRange(min, max);
-                if (refreshRateRange.min == 0 && refreshRateRange.max == 0) {
-                    // requestedMinRefreshRateRange/requestedMaxRefreshRateRange were invalid
-                    refreshRateRange = null;
-                }
-            }
-
-            if (Objects.equals(refreshRateRange,
-                    mAppPreferredRefreshRateRangeByDisplay.get(displayId))) {
-                return;
-            }
-
-            if (refreshRateRange != null) {
-                mAppPreferredRefreshRateRangeByDisplay.put(displayId, refreshRateRange);
-                vote = Vote.forRenderFrameRates(refreshRateRange.min, refreshRateRange.max);
-            } else {
-                mAppPreferredRefreshRateRangeByDisplay.remove(displayId);
-                vote = null;
-            }
-            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
-                    vote);
-        }
-
         private Display.Mode findAppModeByIdLocked(int displayId, int modeId) {
             Display.Mode[] modes = mAppSupportedModesByDisplay.get(displayId);
             if (modes == null) {
@@ -1384,19 +1360,7 @@
 
         private void dumpLocked(PrintWriter pw) {
             pw.println("  AppRequestObserver");
-            pw.println("    mAppRequestedModeByDisplay:");
-            for (int i = 0; i < mAppRequestedModeByDisplay.size(); i++) {
-                final int id = mAppRequestedModeByDisplay.keyAt(i);
-                final Display.Mode mode = mAppRequestedModeByDisplay.valueAt(i);
-                pw.println("    " + id + " -> " + mode);
-            }
-            pw.println("    mAppPreferredRefreshRateRangeByDisplay:");
-            for (int i = 0; i < mAppPreferredRefreshRateRangeByDisplay.size(); i++) {
-                final int id = mAppPreferredRefreshRateRangeByDisplay.keyAt(i);
-                final RefreshRateRange refreshRateRange =
-                        mAppPreferredRefreshRateRangeByDisplay.valueAt(i);
-                pw.println("    " + id + " -> " + refreshRateRange);
-            }
+            pw.println("    mIgnorePreferredRefreshRate: " + mIgnorePreferredRefreshRate);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/mode/RequestedRefreshRateVote.java b/services/core/java/com/android/server/display/mode/RequestedRefreshRateVote.java
new file mode 100644
index 0000000..843cf88
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/RequestedRefreshRateVote.java
@@ -0,0 +1,51 @@
+/*
+ * 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.display.mode;
+
+import android.annotation.NonNull;
+
+import java.util.Objects;
+
+class RequestedRefreshRateVote implements Vote {
+    final float mRefreshRate;
+
+    RequestedRefreshRateVote(float refreshRate) {
+        this.mRefreshRate = refreshRate;
+    }
+
+    @Override
+    public void updateSummary(@NonNull VoteSummary summary) {
+        summary.requestedRefreshRates.add(mRefreshRate);
+    }
+
+    @Override
+    public String toString() {
+        return "RequestedRefreshRateVote{ refreshRate=" + mRefreshRate + " }";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof RequestedRefreshRateVote that)) return false;
+        return Float.compare(mRefreshRate, that.mRefreshRate) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mRefreshRate);
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/SyntheticModeManager.java b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
index 5b6bbc5..a83b939 100644
--- a/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
+++ b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
@@ -32,7 +32,9 @@
  */
 public class SyntheticModeManager {
     private static final float FLOAT_TOLERANCE = 0.01f;
-    private static final float SYNTHETIC_MODE_HIGH_BOUNDARY = 60f + FLOAT_TOLERANCE;
+    private static final float SYNTHETIC_MODE_REFRESH_RATE = 60f;
+    private static final float SYNTHETIC_MODE_HIGH_BOUNDARY =
+            SYNTHETIC_MODE_REFRESH_RATE + FLOAT_TOLERANCE;
 
     private final boolean mSynthetic60HzModesEnabled;
 
@@ -62,7 +64,7 @@
                 nextModeId = mode.getModeId();
             }
 
-            float divisor = mode.getVsyncRate() / 60f;
+            float divisor = mode.getVsyncRate() / SYNTHETIC_MODE_REFRESH_RATE;
             boolean is60HzAchievable = Math.abs(divisor - Math.round(divisor)) < FLOAT_TOLERANCE;
             if (is60HzAchievable) {
                 sizes.put(new Size(mode.getPhysicalWidth(), mode.getPhysicalHeight()),
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index 3e8b3c5..1ec469c 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -186,6 +186,10 @@
         return new BaseModeRefreshRateVote(baseModeRefreshRate);
     }
 
+    static Vote forRequestedRefreshRate(float refreshRate) {
+        return new RequestedRefreshRateVote(refreshRate);
+    }
+
     static Vote forSupportedRefreshRates(List<SupportedModeData> supportedModes) {
         if (supportedModes.isEmpty()) {
             return null;
diff --git a/services/core/java/com/android/server/display/mode/VoteSummary.java b/services/core/java/com/android/server/display/mode/VoteSummary.java
index d4ce892..00a9226 100644
--- a/services/core/java/com/android/server/display/mode/VoteSummary.java
+++ b/services/core/java/com/android/server/display/mode/VoteSummary.java
@@ -23,7 +23,9 @@
 import android.view.SurfaceControl;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 final class VoteSummary {
     private static final float FLOAT_TOLERANCE = SurfaceControl.RefreshRateRange.FLOAT_TOLERANCE;
@@ -38,7 +40,14 @@
     public int minWidth;
     public int minHeight;
     public boolean disableRefreshRateSwitching;
+    /**
+     *  available modes should have mode with specific refresh rate
+     */
     public float appRequestBaseModeRefreshRate;
+    /**
+     * requestRefreshRate should be within [minRenderFrameRate, maxRenderFrameRate] range
+     */
+    public Set<Float> requestedRefreshRates = new HashSet<>();
 
     @Nullable
     public List<SupportedRefreshRatesVote.RefreshRates> supportedRefreshRates;
@@ -329,6 +338,21 @@
             }
             return false;
         }
+
+        for (Float requestedRefreshRate : requestedRefreshRates) {
+            if (requestedRefreshRate < minRenderFrameRate
+                    || requestedRefreshRate > maxRenderFrameRate) {
+                if (mLoggingEnabled) {
+                    Slog.w(TAG, "Requested refreshRate is outside frame rate range"
+                            + ": requestedRefreshRates=" + requestedRefreshRates
+                            + ", requestedRefreshRate=" + requestedRefreshRate
+                            + ", minRenderFrameRate=" + minRenderFrameRate
+                            + ", maxRenderFrameRate=" + maxRenderFrameRate);
+                }
+                return false;
+            }
+        }
+
         return true;
     }
 
@@ -370,6 +394,7 @@
         minHeight = 0;
         disableRefreshRateSwitching = false;
         appRequestBaseModeRefreshRate = 0f;
+        requestedRefreshRates.clear();
         supportedRefreshRates = null;
         supportedModeIds = null;
         if (mLoggingEnabled) {
@@ -393,6 +418,7 @@
                 + ", minHeight=" + minHeight
                 + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
                 + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate
+                + ", requestRefreshRates=" + requestedRefreshRates
                 + ", supportedRefreshRates=" + supportedRefreshRates
                 + ", supportedModeIds=" + supportedModeIds
                 + ", mIsDisplayResolutionRangeVotingEnabled="
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index dbd1e65..6e027c6 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1029,6 +1029,10 @@
 
     /** Helper method for sending feature discovery command */
     private void reportFeatures(boolean isTvDeviceSetting) {
+        // <Report Features> should only be sent for HDMI 2.0
+        if (getCecVersion() < HdmiControlManager.HDMI_CEC_VERSION_2_0) {
+            return;
+        }
         // check if tv device is enabled for tv device specific RC profile setting
         if (isTvDeviceSetting) {
             if (isTvDeviceEnabled()) {
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
index dd6433d..82ecb4a 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMapRepository.java
@@ -16,12 +16,16 @@
 
 package com.android.server.inputmethod;
 
+import android.annotation.AnyThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.annotation.WorkerThread;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.os.Handler;
+import android.os.Process;
+import android.util.IntArray;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -29,6 +33,10 @@
 import com.android.server.LocalServices;
 import com.android.server.pm.UserManagerInternal;
 
+import java.util.ArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
 /**
  * Provides accesses to per-user additional {@link android.view.inputmethod.InputMethodSubtype}
  * persistent storages.
@@ -38,6 +46,152 @@
     @NonNull
     private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>();
 
+    record WriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap,
+                     @NonNull InputMethodMap inputMethodMap) {
+    }
+
+    static final class SingleThreadedBackgroundWriter {
+        /**
+         * A {@link ReentrantLock} used to guard {@link #mPendingTasks} and {@link #mRemovedUsers}.
+         */
+        @NonNull
+        private final ReentrantLock mLock = new ReentrantLock();
+        /**
+         * A {@link Condition} associated with {@link #mLock} for producer to unblock consumer.
+         */
+        @NonNull
+        private final Condition mLockNotifier = mLock.newCondition();
+
+        @GuardedBy("mLock")
+        @NonNull
+        private final SparseArray<WriteTask> mPendingTasks = new SparseArray<>();
+
+        @GuardedBy("mLock")
+        private final IntArray mRemovedUsers = new IntArray();
+
+        @NonNull
+        private final Thread mWriterThread = new Thread("android.ime.as") {
+
+            /**
+             * Waits until the next data has come then return the result after filtering out any
+             * already removed users.
+             *
+             * @return A list of {@link WriteTask} to be written into persistent storage
+             */
+            @WorkerThread
+            private ArrayList<WriteTask> fetchNextTasks() {
+                final SparseArray<WriteTask> tasks;
+                final IntArray removedUsers;
+                mLock.lock();
+                try {
+                    while (true) {
+                        if (mPendingTasks.size() != 0) {
+                            tasks = mPendingTasks.clone();
+                            mPendingTasks.clear();
+                            if (mRemovedUsers.size() == 0) {
+                                removedUsers = null;
+                            } else {
+                                removedUsers = mRemovedUsers.clone();
+                            }
+                            break;
+                        }
+                        mLockNotifier.awaitUninterruptibly();
+                    }
+                } finally {
+                    mLock.unlock();
+                }
+                final int size = tasks.size();
+                final ArrayList<WriteTask> result = new ArrayList<>(size);
+                for (int i = 0; i < size; ++i) {
+                    final int userId = tasks.keyAt(i);
+                    if (removedUsers != null && removedUsers.contains(userId)) {
+                        continue;
+                    }
+                    result.add(tasks.valueAt(i));
+                }
+                return result;
+            }
+
+            @WorkerThread
+            @Override
+            public void run() {
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+                while (true) {
+                    final ArrayList<WriteTask> tasks = fetchNextTasks();
+                    tasks.forEach(task -> AdditionalSubtypeUtils.save(
+                            task.subtypeMap, task.inputMethodMap, task.userId));
+                }
+            }
+        };
+
+        /**
+         * Schedules a write operation
+         *
+         * @param userId the target user ID of this operation
+         * @param subtypeMap {@link AdditionalSubtypeMap} to be saved
+         * @param inputMethodMap {@link InputMethodMap} to be used to filter our {@code subtypeMap}
+         */
+        @AnyThread
+        void scheduleWriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap,
+                @NonNull InputMethodMap inputMethodMap) {
+            final var task = new WriteTask(userId, subtypeMap, inputMethodMap);
+            mLock.lock();
+            try {
+                if (mRemovedUsers.contains(userId)) {
+                    return;
+                }
+                mPendingTasks.put(userId, task);
+                mLockNotifier.signalAll();
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        /**
+         * Called back when a user is being created.
+         *
+         * @param userId The user ID to be created
+         */
+        @AnyThread
+        void onUserCreated(@UserIdInt int userId) {
+            mLock.lock();
+            try {
+                for (int i = mRemovedUsers.size() - 1; i >= 0; --i) {
+                    if (mRemovedUsers.get(i) == userId) {
+                        mRemovedUsers.remove(i);
+                    }
+                }
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        /**
+         * Called back when a user is being removed. Any pending task will be effectively canceled
+         * if the user is removed before the task is fulfilled.
+         *
+         * @param userId The user ID to be removed
+         */
+        @AnyThread
+        void onUserRemoved(@UserIdInt int userId) {
+            mLock.lock();
+            try {
+                mRemovedUsers.add(userId);
+                mPendingTasks.remove(userId);
+            } finally {
+                mLock.unlock();
+            }
+        }
+
+        void startThread() {
+            mWriterThread.start();
+        }
+    }
+
+    private static final SingleThreadedBackgroundWriter sWriter =
+            new SingleThreadedBackgroundWriter();
+
     /**
      * Not intended to be instantiated.
      */
@@ -64,9 +218,11 @@
             return;
         }
         sPerUserMap.put(userId, map);
-        // TODO: Offload this to a background thread.
-        // TODO: Skip if the previous data is exactly the same as new one.
-        AdditionalSubtypeUtils.save(map, inputMethodMap, userId);
+        sWriter.scheduleWriteTask(userId, map, inputMethodMap);
+    }
+
+    static void startWriterThread() {
+        sWriter.startThread();
     }
 
     static void initialize(@NonNull Handler handler, @NonNull Context context) {
@@ -78,6 +234,7 @@
                         @Override
                         public void onUserCreated(UserInfo user, @Nullable Object token) {
                             final int userId = user.id;
+                            sWriter.onUserCreated(userId);
                             handler.post(() -> {
                                 synchronized (ImfLock.class) {
                                     if (!sPerUserMap.contains(userId)) {
@@ -99,6 +256,7 @@
                         @Override
                         public void onUserRemoved(UserInfo user) {
                             final int userId = user.id;
+                            sWriter.onUserRemoved(userId);
                             handler.post(() -> {
                                 synchronized (ImfLock.class) {
                                     sPerUserMap.remove(userId);
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 79f1a9c..c19cb03 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -55,7 +55,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.OptionalInt;
-import java.util.function.IntConsumer;
 
 // TODO(b/210039666): See if we can make this class thread-safe.
 final class HandwritingModeController {
@@ -91,7 +90,6 @@
     private boolean mDelegationConnectionlessFlow;
     private Runnable mDelegationIdleTimeoutRunnable;
     private Handler mDelegationIdleTimeoutHandler;
-    private IntConsumer mPointerToolTypeConsumer;
     private final Runnable mDiscardDelegationTextRunnable;
     private HandwritingEventReceiverSurface mHandwritingSurface;
 
@@ -99,7 +97,7 @@
 
     @AnyThread
     HandwritingModeController(Context context, Looper uiThreadLooper,
-            Runnable inkWindowInitRunnable, IntConsumer toolTypeConsumer,
+            Runnable inkWindowInitRunnable,
             Runnable discardDelegationTextRunnable) {
         mContext = context;
         mLooper = uiThreadLooper;
@@ -109,7 +107,6 @@
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mCurrentRequestId = 0;
         mInkWindowInitRunnable = inkWindowInitRunnable;
-        mPointerToolTypeConsumer = toolTypeConsumer;
         mDiscardDelegationTextRunnable = discardDelegationTextRunnable;
     }
 
@@ -389,16 +386,10 @@
                     "Input Event should not be processed when IME has the spy channel.");
         }
 
-        if (!(ev instanceof MotionEvent)) {
+        if (!(ev instanceof MotionEvent event)) {
             Slog.wtf(TAG, "Received non-motion event in stylus monitor.");
             return false;
         }
-        final MotionEvent event = (MotionEvent) ev;
-        if (mPointerToolTypeConsumer != null && event.getAction() == MotionEvent.ACTION_DOWN) {
-            int toolType = event.getToolType(event.getActionIndex());
-            // notify IME of change in tool type.
-            mPointerToolTypeConsumer.accept(toolType);
-        }
         if (!event.isStylusPointer()) {
             return false;
         }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index dace67f..f61ca61 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -16,6 +16,8 @@
 
 package com.android.server.inputmethod;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
@@ -30,6 +32,9 @@
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 import com.android.server.LocalServices;
 
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
 import java.util.Collections;
 import java.util.List;
 
@@ -38,6 +43,16 @@
  */
 public abstract class InputMethodManagerInternal {
     /**
+     * Indicates that the method is guaranteed to not require {@link ImfLock}.
+     *
+     * <p>You can call this method without worrying about system_server lock layering.</p>
+     */
+    @Retention(SOURCE)
+    @Target({ElementType.METHOD})
+    public @interface ImfLockFree {
+    }
+
+    /**
      * Listener for input method list changed events.
      */
     public interface InputMethodListListener {
@@ -53,6 +68,7 @@
      *
      * @param interactive the interactive mode parameter
      */
+    @ImfLockFree
     public abstract void setInteractive(boolean interactive);
 
     /**
@@ -61,6 +77,7 @@
      * @param reason               the reason for hiding the current input method
      * @param originatingDisplayId the display ID the request is originated
      */
+    @ImfLockFree
     public abstract void hideAllInputMethods(@SoftInputShowHideReason int reason,
             int originatingDisplayId);
 
@@ -143,6 +160,7 @@
      *
      * @param listener the listener to add
      */
+    @ImfLockFree
     public abstract void registerInputMethodListListener(InputMethodListListener listener);
 
     /**
@@ -178,6 +196,7 @@
      *
      * @param displayId the display hosting the IME window
      */
+    @ImfLockFree
     public abstract void removeImeSurface(int displayId);
 
     /**
@@ -188,12 +207,14 @@
      * @param disableImeIcon indicates whether IME icon should be enabled or not
      * @param displayId      the display for which to update the IME window status
      */
+    @ImfLockFree
     public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId);
 
     /**
      * Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if
      * there is an ongoing handwriting session.
      */
+    @ImfLockFree
     public abstract void maybeFinishStylusHandwriting();
 
     /**
@@ -240,10 +261,12 @@
      */
     private static final InputMethodManagerInternal NOP =
             new InputMethodManagerInternal() {
+                @ImfLockFree
                 @Override
                 public void setInteractive(boolean interactive) {
                 }
 
+                @ImfLockFree
                 @Override
                 public void hideAllInputMethods(@SoftInputShowHideReason int reason,
                         int originatingDisplayId) {
@@ -282,6 +305,7 @@
                         int deviceId, @Nullable String imeId) {
                 }
 
+                @ImfLockFree
                 @Override
                 public void registerInputMethodListListener(InputMethodListListener listener) {
                 }
@@ -300,10 +324,12 @@
                 public void onImeParentChanged(int displayId) {
                 }
 
+                @ImfLockFree
                 @Override
                 public void removeImeSurface(int displayId) {
                 }
 
+                @ImfLockFree
                 @Override
                 public void updateImeWindowStatus(boolean disableImeIcon, int displayId) {
                 }
@@ -318,6 +344,7 @@
                         @UserIdInt int userId) {
                 }
 
+                @ImfLockFree
                 @Override
                 public void maybeFinishStylusHandwriting() {
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 5843d72..d22438f 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -205,7 +205,6 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
-import java.util.function.IntConsumer;
 import java.util.function.IntFunction;
 
 /**
@@ -1305,12 +1304,9 @@
                     com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor);
             mNonPreemptibleInputMethods = mRes.getStringArray(
                     com.android.internal.R.array.config_nonPreemptibleInputMethods);
-            IntConsumer toolTypeConsumer =
-                    Flags.useHandwritingListenerForTooltype()
-                            ? toolType -> onUpdateEditorToolType(toolType) : null;
             Runnable discardDelegationTextRunnable = () -> discardHandwritingDelegationText();
             mHwController = new HandwritingModeController(mContext, thread.getLooper(),
-                    new InkWindowInitializer(), toolTypeConsumer, discardDelegationTextRunnable);
+                    new InkWindowInitializer(), discardDelegationTextRunnable);
             registerDeviceListenerAndCheckStylusSupport();
         }
     }
@@ -1547,6 +1543,10 @@
                 InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                         getPackageManagerForUser(mContext, currentUserId),
                         newSettings.getEnabledInputMethodList());
+
+                final var unused = SystemServerInitThreadPool.submit(
+                        AdditionalSubtypeMapRepository::startWriterThread,
+                        "Start AdditionalSubtypeMapRepository's writer thread");
             }
         }
     }
@@ -3412,8 +3412,10 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
             mCurStatsToken = null;
 
-            if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
-                curMethod.updateEditorToolType(lastClickToolType);
+            if (Flags.useHandwritingListenerForTooltype()) {
+                maybeReportToolType();
+            } else if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+                onUpdateEditorToolType(lastClickToolType);
             }
             mVisibilityApplier.performShowIme(windowToken, statsToken,
                     mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
@@ -3427,6 +3429,29 @@
         return false;
     }
 
+    @GuardedBy("ImfLock.class")
+    private void maybeReportToolType() {
+        int lastDeviceId = mInputManagerInternal.getLastUsedInputDeviceId();
+        final InputManager im = mContext.getSystemService(InputManager.class);
+        if (im == null) {
+            return;
+        }
+        InputDevice device = im.getInputDevice(lastDeviceId);
+        if (device == null) {
+            return;
+        }
+        int toolType;
+        if (isStylusDevice(device)) {
+            toolType = MotionEvent.TOOL_TYPE_STYLUS;
+        } else if (isFingerDevice(device)) {
+            toolType = MotionEvent.TOOL_TYPE_FINGER;
+        } else {
+            // other toolTypes are irrelevant and reported as unknown.
+            toolType = MotionEvent.TOOL_TYPE_UNKNOWN;
+        }
+        onUpdateEditorToolType(toolType);
+    }
+
     @Override
     public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
             @NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
@@ -4281,6 +4306,10 @@
                 || inputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS);
     }
 
+    private static boolean isFingerDevice(InputDevice inputDevice) {
+        return inputDevice.supportsSource(InputDevice.SOURCE_TOUCHSCREEN);
+    }
+
     @GuardedBy("ImfLock.class")
     private boolean hasSupportedStylusLocked() {
         return mStylusIds != null && mStylusIds.size() != 0;
@@ -5471,12 +5500,14 @@
 
     private final class LocalServiceImpl extends InputMethodManagerInternal {
 
+        @ImfLockFree
         @Override
         public void setInteractive(boolean interactive) {
             // Do everything in handler so as not to block the caller.
             mHandler.obtainMessage(MSG_SET_INTERACTIVE, interactive ? 1 : 0, 0).sendToTarget();
         }
 
+        @ImfLockFree
         @Override
         public void hideAllInputMethods(@SoftInputShowHideReason int reason,
                 int originatingDisplayId) {
@@ -5562,6 +5593,7 @@
             }
         }
 
+        @ImfLockFree
         @Override
         public void registerInputMethodListListener(InputMethodListListener listener) {
             mInputMethodListListeners.addIfAbsent(listener);
@@ -5609,11 +5641,13 @@
             }
         }
 
+        @ImfLockFree
         @Override
         public void removeImeSurface(int displayId) {
             mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget();
         }
 
+        @ImfLockFree
         @Override
         public void updateImeWindowStatus(boolean disableImeIcon, int displayId) {
             mHandler.obtainMessage(MSG_UPDATE_IME_WINDOW_STATUS, disableImeIcon ? 1 : 0, 0)
@@ -5691,6 +5725,7 @@
             }
         }
 
+        @ImfLockFree
         @Override
         public void maybeFinishStylusHandwriting() {
             mHandler.removeMessages(MSG_FINISH_HANDWRITING);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 1c958a9..23f947c 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -367,9 +367,9 @@
         }
 
         protected void dump(final Printer pw, final String prefix) {
-            for (int i = 0; i < mUsageHistoryOfSubtypeListItemIndex.length; ++i) {
-                final int rank = mUsageHistoryOfSubtypeListItemIndex[i];
-                final ImeSubtypeListItem item = mImeSubtypeList.get(i);
+            for (int rank = 0; rank < mUsageHistoryOfSubtypeListItemIndex.length; ++rank) {
+                final int index = mUsageHistoryOfSubtypeListItemIndex[rank];
+                final ImeSubtypeListItem item = mImeSubtypeList.get(index);
                 pw.println(prefix + "rank=" + rank + " item=" + item);
             }
         }
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 563f93e..b9e0960 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -84,9 +84,16 @@
      * from the delegate selector.
      */
     private static final String LOCALES_FROM_DELEGATE_PREFS = "LocalesFromDelegatePrefs.xml";
+    private static final String LOCALES_STAGED_DATA_PREFS = "LocalesStagedDataPrefs.xml";
+    private static final String ARCHIVED_PACKAGES_PREFS = "ArchivedPackagesPrefs.xml";
     // Stage data would be deleted on reboot since it's stored in memory. So it's retained until
     // retention period OR next reboot, whichever happens earlier.
     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
+    // Store the locales staged data for the specified package in the SharedPreferences. The format
+    // is locales s:setFromDelegate
+    // For example: en-US s:true
+    private static final String STRING_SPLIT = " s:";
+    private static final String KEY_STAGED_DATA_TIME = "staged_data_time";
 
     private final LocaleManagerService mLocaleManagerService;
     private final PackageManager mPackageManager;
@@ -94,39 +101,34 @@
     private final Context mContext;
     private final Object mStagedDataLock = new Object();
 
-    // Staged data map keyed by user-id to handle multi-user scenario / work profiles. We are using
-    // SparseArray because it is more memory-efficient than a HashMap.
-    private final SparseArray<StagedData> mStagedData;
-
     // SharedPreferences to store packages whose app-locale was set by a delegate, as opposed to
     // the application setting the app-locale itself.
     private final SharedPreferences mDelegateAppLocalePackages;
+    // For unit tests
+    private final SparseArray<File> mStagedDataFiles;
+    private final File mArchivedPackagesFile;
+
     private final BroadcastReceiver mUserMonitor;
-    // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon receiving
-    // the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform the data
-    // restoration during the second PACKAGE_ADDED broadcast, which is sent subsequently when the
-    // app is installed.
-    private final Set<String> mPkgsToRestore;
 
     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
             PackageManager packageManager, HandlerThread broadcastHandlerThread) {
         this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
-                new SparseArray<>(), broadcastHandlerThread, null);
+                broadcastHandlerThread, null, null, null);
     }
 
-    @VisibleForTesting LocaleManagerBackupHelper(Context context,
-            LocaleManagerService localeManagerService,
-            PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
-            HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
+    @VisibleForTesting
+    LocaleManagerBackupHelper(Context context, LocaleManagerService localeManagerService,
+            PackageManager packageManager, Clock clock, HandlerThread broadcastHandlerThread,
+            SparseArray<File> stagedDataFiles, File archivedPackagesFile,
+            SharedPreferences delegateAppLocalePackages) {
         mContext = context;
         mLocaleManagerService = localeManagerService;
         mPackageManager = packageManager;
         mClock = clock;
-        mStagedData = stagedData;
         mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
-                : createPersistedInfo();
-        mPkgsToRestore = new ArraySet<>();
-
+            : createPersistedInfo();
+        mArchivedPackagesFile = archivedPackagesFile;
+        mStagedDataFiles = stagedDataFiles;
         mUserMonitor = new UserMonitor();
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_USER_REMOVED);
@@ -148,7 +150,7 @@
         }
 
         synchronized (mStagedDataLock) {
-            cleanStagedDataForOldEntriesLocked();
+            cleanStagedDataForOldEntriesLocked(userId);
         }
 
         HashMap<String, LocalesInfo> pkgStates = new HashMap<>();
@@ -207,14 +209,11 @@
         return out.toByteArray();
     }
 
-    private void cleanStagedDataForOldEntriesLocked() {
-        for (int i = 0; i < mStagedData.size(); i++) {
-            int userId = mStagedData.keyAt(i);
-            StagedData stagedData = mStagedData.get(userId);
-            if (stagedData.mCreationTimeMillis
-                    < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
-                deleteStagedDataLocked(userId);
-            }
+    private void cleanStagedDataForOldEntriesLocked(@UserIdInt int userId) {
+        Long created_time = getStagedDataSp(userId).getLong(KEY_STAGED_DATA_TIME, -1);
+        if (created_time != -1
+                && created_time < mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
+            deleteStagedDataLocked(userId);
         }
     }
 
@@ -252,20 +251,16 @@
         // performed simultaneously.
         synchronized (mStagedDataLock) {
             // Backups for apps which are yet to be installed.
-            StagedData stagedData = new StagedData(mClock.millis(), new HashMap<>());
-
             for (String pkgName : pkgStates.keySet()) {
                 LocalesInfo localesInfo = pkgStates.get(pkgName);
                 // Check if the application is already installed for the concerned user.
                 if (isPackageInstalledForUser(pkgName, userId)) {
-                    if (mPkgsToRestore != null) {
-                        mPkgsToRestore.remove(pkgName);
-                    }
+                    removeFromArchivedPackagesInfo(userId, pkgName);
                     // Don't apply the restore if the locales have already been set for the app.
                     checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
                 } else {
                     // Stage the data if the app isn't installed.
-                    stagedData.mPackageStates.put(pkgName, localesInfo);
+                    storeStagedDataInfo(userId, pkgName, localesInfo);
                     if (DEBUG) {
                         Slog.d(TAG, "Add locales=" + localesInfo.mLocales
                                 + " fromDelegate=" + localesInfo.mSetFromDelegate
@@ -274,8 +269,9 @@
                 }
             }
 
-            if (!stagedData.mPackageStates.isEmpty()) {
-                mStagedData.put(userId, stagedData);
+            // Create the time if the data is being staged.
+            if (!getStagedDataSp(userId).getAll().isEmpty()) {
+                storeStagedDataCreatedTime(userId);
             }
         }
     }
@@ -293,14 +289,23 @@
      * added on device.
      */
     void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
-        boolean archived = false;
+        int userId = UserHandle.getUserId(uid);
         if (extras != null) {
-            archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
-            if (archived && mPkgsToRestore != null) {
-                mPkgsToRestore.add(packageName);
+            // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon
+            // receiving the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform
+            // the data restoration during the second PACKAGE_ADDED broadcast, which is sent
+            // subsequently when the app is installed.
+            boolean archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
+            if (DEBUG) {
+                Slog.d(TAG,
+                        "onPackageAddedWithExtras packageName: " + packageName + ", userId: "
+                                + userId + ", archived: " + archived);
+            }
+            if (archived) {
+                addInArchivedPackagesInfo(userId, packageName);
             }
         }
-        checkStageDataAndApplyRestore(packageName, uid);
+        checkStageDataAndApplyRestore(packageName, userId);
     }
 
     /**
@@ -310,9 +315,32 @@
      */
     void onPackageUpdateFinished(String packageName, int uid) {
         int userId = UserHandle.getUserId(uid);
-        if (mPkgsToRestore != null && mPkgsToRestore.contains(packageName)) {
-            mPkgsToRestore.remove(packageName);
-            checkStageDataAndApplyRestore(packageName, uid);
+        if (DEBUG) {
+            Slog.d(TAG,
+                    "onPackageUpdateFinished userId: " + userId + ", packageName: " + packageName);
+        }
+        String user = Integer.toString(userId);
+        File file = getArchivedPackagesFile();
+        if (file.exists()) {
+            SharedPreferences sp = getArchivedPackagesSp(file);
+            Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
+            if (packageNames.remove(packageName)) {
+                SharedPreferences.Editor editor = sp.edit();
+                if (packageNames.isEmpty()) {
+                    if (!editor.remove(user).commit()) {
+                        Slog.e(TAG, "Failed to remove the user");
+                    }
+                    if (sp.getAll().isEmpty()) {
+                        file.delete();
+                    }
+                } else {
+                    // commit and log the result.
+                    if (!editor.putStringSet(user, packageNames).commit()) {
+                        Slog.e(TAG, "failed to remove the package");
+                    }
+                }
+                checkStageDataAndApplyRestore(packageName, userId);
+            }
         }
         cleanApplicationLocalesIfNeeded(packageName, userId);
     }
@@ -347,16 +375,16 @@
         }
     }
 
-    private void checkStageDataAndApplyRestore(String packageName, int uid) {
+    private void checkStageDataAndApplyRestore(String packageName, int userId) {
         try {
             synchronized (mStagedDataLock) {
-                cleanStagedDataForOldEntriesLocked();
-
-                int userId = UserHandle.getUserId(uid);
-                if (mStagedData.contains(userId)) {
-                    if (mPkgsToRestore != null) {
-                        mPkgsToRestore.remove(packageName);
+                cleanStagedDataForOldEntriesLocked(userId);
+                if (!getStagedDataSp(userId).getString(packageName, "").isEmpty()) {
+                    if (DEBUG) {
+                        Slog.d(TAG,
+                                "checkStageDataAndApplyRestore, remove package and restore data");
                     }
+                    removeFromArchivedPackagesInfo(userId, packageName);
                     // Perform lazy restore only if the staged data exists.
                     doLazyRestoreLocked(packageName, userId);
                 }
@@ -417,8 +445,17 @@
         }
     }
 
-    private void deleteStagedDataLocked(@UserIdInt int userId) {
-        mStagedData.remove(userId);
+    void deleteStagedDataLocked(@UserIdInt int userId) {
+        File stagedFile = getStagedDataFile(userId);
+        SharedPreferences sp = getStagedDataSp(stagedFile);
+        // commit and log the result.
+        if (!sp.edit().clear().commit()) {
+            Slog.e(TAG, "Failed to commit data!");
+        }
+
+        if (stagedFile.exists()) {
+            stagedFile.delete();
+        }
     }
 
     /**
@@ -473,16 +510,6 @@
         out.endDocument();
     }
 
-    static class StagedData {
-        final long mCreationTimeMillis;
-        final HashMap<String, LocalesInfo> mPackageStates;
-
-        StagedData(long creationTimeMillis, HashMap<String, LocalesInfo> pkgStates) {
-            mCreationTimeMillis = creationTimeMillis;
-            mPackageStates = pkgStates;
-        }
-    }
-
     static class LocalesInfo {
         final String mLocales;
         final boolean mSetFromDelegate;
@@ -508,6 +535,7 @@
                     synchronized (mStagedDataLock) {
                         deleteStagedDataLocked(userId);
                         removeProfileFromPersistedInfo(userId);
+                        removeArchivedPackagesForUser(userId);
                     }
                 }
             } catch (Exception e) {
@@ -533,26 +561,159 @@
             return;
         }
 
-        StagedData stagedData = mStagedData.get(userId);
-        for (String pkgName : stagedData.mPackageStates.keySet()) {
-            LocalesInfo localesInfo = stagedData.mPackageStates.get(pkgName);
-
-            if (pkgName.equals(packageName)) {
-
-                checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
-
-                // Remove the restored entry from the staged data list.
-                stagedData.mPackageStates.remove(pkgName);
-
-                // Remove the stage data entry for user if there are no more packages to restore.
-                if (stagedData.mPackageStates.isEmpty()) {
-                    mStagedData.remove(userId);
-                }
-
-                // No need to loop further after restoring locales because the staged data will
-                // contain at most one entry for the newly added package.
-                break;
+        SharedPreferences sp = getStagedDataSp(userId);
+        String value = sp.getString(packageName, "");
+        if (!value.isEmpty()) {
+            String[] info = value.split(STRING_SPLIT);
+            if (info == null || info.length != 2) {
+                Slog.e(TAG, "Failed to restore data");
+                return;
             }
+            LocalesInfo localesInfo = new LocalesInfo(info[0], Boolean.parseBoolean(info[1]));
+            checkExistingLocalesAndApplyRestore(packageName, localesInfo, userId);
+
+            // Remove the restored entry from the staged data list.
+            if (!sp.edit().remove(packageName).commit()) {
+                Slog.e(TAG, "Failed to commit data!");
+            }
+        }
+
+        // Remove the stage data entry for user if there are no more packages to restore.
+        if (sp.getAll().size() == 1 && sp.getLong(KEY_STAGED_DATA_TIME, -1) != -1) {
+            deleteStagedDataLocked(userId);
+        }
+    }
+
+    private File getStagedDataFile(@UserIdInt int userId) {
+        return mStagedDataFiles == null ? new File(Environment.getDataSystemDeDirectory(userId),
+            LOCALES_STAGED_DATA_PREFS) : mStagedDataFiles.get(userId);
+    }
+
+    private SharedPreferences getStagedDataSp(File file) {
+        return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext()
+            .getSharedPreferences(file, Context.MODE_PRIVATE)
+            : mContext.getSharedPreferences(file, Context.MODE_PRIVATE);
+    }
+
+    private SharedPreferences getStagedDataSp(@UserIdInt int userId) {
+        return mStagedDataFiles == null ? mContext.createDeviceProtectedStorageContext()
+            .getSharedPreferences(getStagedDataFile(userId), Context.MODE_PRIVATE)
+            : mContext.getSharedPreferences(mStagedDataFiles.get(userId), Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Store the staged locales info.
+     */
+    private void storeStagedDataInfo(@UserIdInt int userId, @NonNull String packageName,
+            @NonNull LocalesInfo localesInfo) {
+        if (DEBUG) {
+            Slog.d(TAG, "storeStagedDataInfo, userId: " + userId + ", packageName: " + packageName
+                    + ", localesInfo.mLocales: " + localesInfo.mLocales
+                    + ", localesInfo.mSetFromDelegate: " + localesInfo.mSetFromDelegate);
+        }
+        String info =
+                localesInfo.mLocales + STRING_SPLIT + String.valueOf(localesInfo.mSetFromDelegate);
+        SharedPreferences sp = getStagedDataSp(userId);
+        // commit and log the result.
+        if (!sp.edit().putString(packageName, info).commit()) {
+            Slog.e(TAG, "Failed to commit data!");
+        }
+    }
+
+    /**
+     * Store the time of creation for staged locales info.
+     */
+    private void storeStagedDataCreatedTime(@UserIdInt int userId) {
+        SharedPreferences sp = getStagedDataSp(userId);
+        // commit and log the result.
+        if (!sp.edit().putLong(KEY_STAGED_DATA_TIME, mClock.millis()).commit()) {
+            Slog.e(TAG, "Failed to commit data!");
+        }
+    }
+
+    private File getArchivedPackagesFile() {
+        return mArchivedPackagesFile == null ? new File(
+            Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM),
+            ARCHIVED_PACKAGES_PREFS) : mArchivedPackagesFile;
+    }
+
+    private SharedPreferences getArchivedPackagesSp(File file) {
+        return mArchivedPackagesFile == null ? mContext.createDeviceProtectedStorageContext()
+            .getSharedPreferences(file, Context.MODE_PRIVATE)
+            : mContext.getSharedPreferences(file, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Add the package into the archived packages list.
+     */
+    private void addInArchivedPackagesInfo(@UserIdInt int userId, @NonNull String packageName) {
+        String user = Integer.toString(userId);
+        SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile());
+        Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
+        if (DEBUG) {
+            Slog.d(TAG, "addInArchivedPackagesInfo before packageNames: " + packageNames
+                    + ", packageName: " + packageName);
+        }
+        if (packageNames.add(packageName)) {
+            // commit and log the result.
+            if (!sp.edit().putStringSet(user, packageNames).commit()) {
+                Slog.e(TAG, "failed to add the package");
+            }
+        }
+    }
+
+    /**
+     * Remove the package from the archived packages list.
+     */
+    private void removeFromArchivedPackagesInfo(@UserIdInt int userId,
+            @NonNull String packageName) {
+        File file = getArchivedPackagesFile();
+        if (file.exists()) {
+            String user = Integer.toString(userId);
+            SharedPreferences sp = getArchivedPackagesSp(getArchivedPackagesFile());
+            Set<String> packageNames = new ArraySet<>(sp.getStringSet(user, new ArraySet<>()));
+            if (DEBUG) {
+                Slog.d(TAG, "removeFromArchivedPackagesInfo before packageNames: " + packageNames
+                        + ", packageName: " + packageName);
+            }
+            if (packageNames.remove(packageName)) {
+                SharedPreferences.Editor editor = sp.edit();
+                if (packageNames.isEmpty()) {
+                    if (!editor.remove(user).commit()) {
+                        Slog.e(TAG, "Failed to remove user");
+                    }
+                    if (sp.getAll().isEmpty()) {
+                        file.delete();
+                    }
+                } else {
+                    // commit and log the result.
+                    if (!editor.putStringSet(user, packageNames).commit()) {
+                        Slog.e(TAG, "failed to remove the package");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove the user from the archived packages list.
+     */
+    private void removeArchivedPackagesForUser(@UserIdInt int userId) {
+        String user = Integer.toString(userId);
+        File file = getArchivedPackagesFile();
+        SharedPreferences sp = getArchivedPackagesSp(file);
+
+        if (sp == null || !sp.contains(user)) {
+            Slog.w(TAG, "The profile is not existed in the archived package info");
+            return;
+        }
+
+        if (!sp.edit().remove(user).commit()) {
+            Slog.e(TAG, "Failed to remove user");
+        }
+
+        if (sp.getAll().isEmpty() && file.exists()) {
+            file.delete();
         }
     }
 
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index b3ab229..ae3d36a 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -593,7 +593,7 @@
         public RebootEscrowManager getRebootEscrowManager(RebootEscrowManager.Callbacks callbacks,
                 LockSettingsStorage storage) {
             return new RebootEscrowManager(mContext, callbacks, storage,
-                    getHandler(getServiceThread()));
+                    getHandler(getServiceThread()), getUserManagerInternal());
         }
 
         public int binderGetCallingUid() {
diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
index e1cd2c5..d0b8990 100644
--- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
+++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
@@ -51,16 +51,20 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.widget.RebootEscrowListener;
+import com.android.server.pm.UserManagerInternal;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
+import java.util.Set;
 
 import javax.crypto.SecretKey;
 
@@ -138,6 +142,7 @@
             ERROR_KEYSTORE_FAILURE,
             ERROR_NO_NETWORK,
             ERROR_TIMEOUT_EXHAUSTED,
+            ERROR_NO_REBOOT_ESCROW_DATA,
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface RebootEscrowErrorCode {
@@ -153,6 +158,7 @@
     static final int ERROR_KEYSTORE_FAILURE = 7;
     static final int ERROR_NO_NETWORK = 8;
     static final int ERROR_TIMEOUT_EXHAUSTED = 9;
+    static final int ERROR_NO_REBOOT_ESCROW_DATA = 10;
 
     private @RebootEscrowErrorCode int mLoadEscrowDataErrorCode = ERROR_NONE;
 
@@ -222,11 +228,16 @@
         private final RebootEscrowKeyStoreManager mKeyStoreManager;
         private final LockSettingsStorage mStorage;
         private RebootEscrowProviderInterface mRebootEscrowProvider;
+        private final UserManagerInternal mUserManagerInternal;
 
-        Injector(Context context, LockSettingsStorage storage) {
+        Injector(
+                Context context,
+                LockSettingsStorage storage,
+                UserManagerInternal userManagerInternal) {
             mContext = context;
             mStorage = storage;
             mKeyStoreManager = new RebootEscrowKeyStoreManager();
+            mUserManagerInternal = userManagerInternal;
         }
 
         private RebootEscrowProviderInterface createRebootEscrowProvider() {
@@ -326,6 +337,10 @@
             return (UserManager) mContext.getSystemService(Context.USER_SERVICE);
         }
 
+        public UserManagerInternal getUserManagerInternal() {
+            return mUserManagerInternal;
+        }
+
         public RebootEscrowKeyStoreManager getKeyStoreManager() {
             return mKeyStoreManager;
         }
@@ -402,8 +417,8 @@
     }
 
     RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage,
-            Handler handler) {
-        this(new Injector(context, storage), callbacks, storage, handler);
+                        Handler handler, UserManagerInternal userManagerInternal) {
+        this(new Injector(context, storage, userManagerInternal), callbacks, storage, handler);
     }
 
     @VisibleForTesting
@@ -451,18 +466,50 @@
         onEscrowRestoreComplete(false, attemptCount, retryHandler);
     }
 
-    void loadRebootEscrowDataIfAvailable(Handler retryHandler) {
-        List<UserInfo> users = mUserManager.getUsers();
-        List<UserInfo> rebootEscrowUsers = new ArrayList<>();
+    private List<UserInfo> getUsersToUnlock(List<UserInfo> users) {
+        // System user must be unlocked to unlock any other user
+        if (mCallbacks.isUserSecure(USER_SYSTEM) && !mStorage.hasRebootEscrow(USER_SYSTEM)) {
+            Slog.i(TAG, "No reboot escrow data found for system user");
+            return Collections.emptyList();
+        }
+
+        Set<Integer> noEscrowDataUsers = new HashSet<>();
         for (UserInfo user : users) {
-            if (mCallbacks.isUserSecure(user.id) && mStorage.hasRebootEscrow(user.id)) {
-                rebootEscrowUsers.add(user);
+            if (mCallbacks.isUserSecure(user.id)
+                    && !mStorage.hasRebootEscrow(user.id)) {
+                Slog.d(TAG, "No reboot escrow data found for user " + user);
+                noEscrowDataUsers.add(user.id);
             }
         }
 
+        List<UserInfo> rebootEscrowUsers = new ArrayList<>();
+        for (UserInfo user : users) {
+            // No lskf, no need to unlock.
+            if (!mCallbacks.isUserSecure(user.id)) {
+                continue;
+            }
+            // Don't unlock if user or user's parent does not have reboot data
+            int userId = user.id;
+            if (noEscrowDataUsers.contains(userId)
+                    || noEscrowDataUsers.contains(
+                            mInjector.getUserManagerInternal().getProfileParentId(userId))) {
+                continue;
+            }
+            rebootEscrowUsers.add(user);
+        }
+        return rebootEscrowUsers;
+    }
+
+    void loadRebootEscrowDataIfAvailable(Handler retryHandler) {
+        List<UserInfo> users = mUserManager.getUsers();
+        List<UserInfo> rebootEscrowUsers = getUsersToUnlock(users);
+
         if (rebootEscrowUsers.isEmpty()) {
             Slog.i(TAG, "No reboot escrow data found for users,"
                     + " skipping loading escrow data");
+            setLoadEscrowDataErrorCode(ERROR_NO_REBOOT_ESCROW_DATA, retryHandler);
+            reportMetricOnRestoreComplete(
+                    /* success= */ false, /* attemptCount= */ 1, retryHandler);
             clearMetricsStorage();
             return;
         }
diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java
index 363684f..09605fe 100644
--- a/services/core/java/com/android/server/media/MediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java
@@ -59,7 +59,7 @@
     public abstract void requestCreateSession(
             long requestId,
             String packageName,
-            String routeId,
+            String routeOriginalId,
             @Nullable Bundle sessionHints,
             @RoutingSessionInfo.TransferReason int transferReason,
             @NonNull UserHandle transferInitiatorUserHandle,
@@ -77,13 +77,15 @@
             long requestId,
             @NonNull UserHandle transferInitiatorUserHandle,
             @NonNull String transferInitiatorPackageName,
-            String sessionId,
-            String routeId,
+            String sessionOriginalId,
+            String routeOriginalId,
             @RoutingSessionInfo.TransferReason int transferReason);
 
-    public abstract void setRouteVolume(long requestId, String routeId, int volume);
-    public abstract void setSessionVolume(long requestId, String sessionId, int volume);
-    public abstract void prepareReleaseSession(@NonNull String sessionId);
+    public abstract void setRouteVolume(long requestId, String routeOriginalId, int volume);
+
+    public abstract void setSessionVolume(long requestId, String sessionOriginalId, int volume);
+
+    public abstract void prepareReleaseSession(@NonNull String sessionUniqueId);
 
     @NonNull
     public String getUniqueId() {
@@ -197,8 +199,8 @@
          */
         public final long mRequestId;
 
-        /** The {@link MediaRoute2Info#getId() id} of the target route. */
-        @NonNull public final String mTargetRouteId;
+        /** The {@link MediaRoute2Info#getOriginalId()} original id} of the target route. */
+        @NonNull public final String mTargetOriginalRouteId;
 
         @RoutingSessionInfo.TransferReason public final int mTransferReason;
 
@@ -209,23 +211,23 @@
 
         SessionCreationOrTransferRequest(
                 long requestId,
-                @NonNull String routeId,
+                @NonNull String targetOriginalRouteId,
                 @RoutingSessionInfo.TransferReason int transferReason,
                 @NonNull UserHandle transferInitiatorUserHandle,
                 @NonNull String transferInitiatorPackageName) {
             mRequestId = requestId;
-            mTargetRouteId = routeId;
+            mTargetOriginalRouteId = targetOriginalRouteId;
             mTransferReason = transferReason;
             mTransferInitiatorUserHandle = transferInitiatorUserHandle;
             mTransferInitiatorPackageName = transferInitiatorPackageName;
         }
 
         public boolean isTargetRoute(@Nullable MediaRoute2Info route2Info) {
-            return route2Info != null && mTargetRouteId.equals(route2Info.getId());
+            return route2Info != null && mTargetOriginalRouteId.equals(route2Info.getOriginalId());
         }
 
-        public boolean isTargetRouteIdInList(@NonNull List<String> routesList) {
-            return routesList.stream().anyMatch(mTargetRouteId::equals);
+        public boolean isTargetRouteIdInList(@NonNull List<String> routeOriginalIdList) {
+            return routeOriginalIdList.stream().anyMatch(mTargetOriginalRouteId::equals);
         }
     }
 }
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index 386657e..71cbcb9 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -103,13 +103,14 @@
     public void requestCreateSession(
             long requestId,
             String packageName,
-            String routeId,
+            String routeOriginalId,
             Bundle sessionHints,
             @RoutingSessionInfo.TransferReason int transferReason,
             @NonNull UserHandle transferInitiatorUserHandle,
             @NonNull String transferInitiatorPackageName) {
         if (mConnectionReady) {
-            mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints);
+            mActiveConnection.requestCreateSession(
+                    requestId, packageName, routeOriginalId, sessionHints);
             updateBinding();
         }
     }
@@ -153,35 +154,35 @@
             long requestId,
             @NonNull UserHandle transferInitiatorUserHandle,
             @NonNull String transferInitiatorPackageName,
-            String sessionId,
-            String routeId,
+            String sessionOriginalId,
+            String routeOriginalId,
             @RoutingSessionInfo.TransferReason int transferReason) {
         if (mConnectionReady) {
-            mActiveConnection.transferToRoute(requestId, sessionId, routeId);
+            mActiveConnection.transferToRoute(requestId, sessionOriginalId, routeOriginalId);
         }
     }
 
     @Override
-    public void setRouteVolume(long requestId, String routeId, int volume) {
+    public void setRouteVolume(long requestId, String routeOriginalId, int volume) {
         if (mConnectionReady) {
-            mActiveConnection.setRouteVolume(requestId, routeId, volume);
+            mActiveConnection.setRouteVolume(requestId, routeOriginalId, volume);
             updateBinding();
         }
     }
 
     @Override
-    public void setSessionVolume(long requestId, String sessionId, int volume) {
+    public void setSessionVolume(long requestId, String sessionOriginalId, int volume) {
         if (mConnectionReady) {
-            mActiveConnection.setSessionVolume(requestId, sessionId, volume);
+            mActiveConnection.setSessionVolume(requestId, sessionOriginalId, volume);
             updateBinding();
         }
     }
 
     @Override
-    public void prepareReleaseSession(@NonNull String sessionId) {
+    public void prepareReleaseSession(@NonNull String sessionUniqueId) {
         synchronized (mLock) {
             for (RoutingSessionInfo session : mSessionInfos) {
-                if (TextUtils.equals(session.getId(), sessionId)) {
+                if (TextUtils.equals(session.getId(), sessionUniqueId)) {
                     mSessionInfos.remove(session);
                     mReleasingSessions.add(session);
                     break;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 76930a0..6b409ee 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -158,20 +158,20 @@
     public void requestCreateSession(
             long requestId,
             String packageName,
-            String routeId,
+            String routeOriginalId,
             Bundle sessionHints,
             @RoutingSessionInfo.TransferReason int transferReason,
             @NonNull UserHandle transferInitiatorUserHandle,
             @NonNull String transferInitiatorPackageName) {
         // Assume a router without MODIFY_AUDIO_ROUTING permission can't request with
         // a route ID different from the default route ID. The service should've filtered.
-        if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+        if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             mCallback.onSessionCreated(this, requestId, mDefaultSessionInfo);
             return;
         }
 
         if (!Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
-            if (TextUtils.equals(routeId, mSelectedRouteId)) {
+            if (TextUtils.equals(routeOriginalId, mSelectedRouteId)) {
                 RoutingSessionInfo currentSessionInfo;
                 synchronized (mLock) {
                     currentSessionInfo = mSessionInfos.get(0);
@@ -192,7 +192,7 @@
             mPendingSessionCreationOrTransferRequest =
                     new SessionCreationOrTransferRequest(
                             requestId,
-                            routeId,
+                            routeOriginalId,
                             RoutingSessionInfo.TRANSFER_REASON_FALLBACK,
                             transferInitiatorUserHandle,
                             transferInitiatorPackageName);
@@ -204,7 +204,7 @@
                 transferInitiatorUserHandle,
                 transferInitiatorPackageName,
                 SYSTEM_SESSION_ID,
-                routeId,
+                routeOriginalId,
                 transferReason);
     }
 
@@ -234,15 +234,15 @@
             long requestId,
             @NonNull UserHandle transferInitiatorUserHandle,
             @NonNull String transferInitiatorPackageName,
-            String sessionId,
-            String routeId,
+            String sessionOriginalId,
+            String routeOriginalId,
             @RoutingSessionInfo.TransferReason int transferReason) {
         String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId();
-        if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+        if (TextUtils.equals(routeOriginalId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
                 // Transfer to the default route (which is the selected route). We replace the id to
                 // be the selected route id so that the transfer reason gets updated.
-                routeId = selectedDeviceRouteId;
+                routeOriginalId = selectedDeviceRouteId;
             } else {
                 Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
                 return;
@@ -254,18 +254,18 @@
                 mPendingTransferRequest =
                         new SessionCreationOrTransferRequest(
                                 requestId,
-                                routeId,
+                                routeOriginalId,
                                 transferReason,
                                 transferInitiatorUserHandle,
                                 transferInitiatorPackageName);
             }
         }
 
-        String finalRouteId = routeId; // Make a final copy to use it in the lambda.
+        String finalRouteId = routeOriginalId; // Make a final copy to use it in the lambda.
         boolean isAvailableDeviceRoute =
                 mDeviceRouteController.getAvailableRoutes().stream()
                         .anyMatch(it -> it.getId().equals(finalRouteId));
-        boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId);
+        boolean isSelectedDeviceRoute = TextUtils.equals(routeOriginalId, selectedDeviceRouteId);
 
         if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
             // The requested route is managed by the device route controller. Note that the selected
@@ -273,12 +273,12 @@
             // of the routing session). If the selected device route is transferred to, we need to
             // make the bluetooth routes inactive so that the device route becomes the selected
             // route of the routing session.
-            mDeviceRouteController.transferTo(routeId);
+            mDeviceRouteController.transferTo(routeOriginalId);
             mBluetoothRouteController.transferTo(null);
         } else {
             // The requested route is managed by the bluetooth route controller.
             mDeviceRouteController.transferTo(null);
-            mBluetoothRouteController.transferTo(routeId);
+            mBluetoothRouteController.transferTo(routeOriginalId);
         }
 
         if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
@@ -288,20 +288,20 @@
     }
 
     @Override
-    public void setRouteVolume(long requestId, String routeId, int volume) {
-        if (!TextUtils.equals(routeId, mSelectedRouteId)) {
+    public void setRouteVolume(long requestId, String routeOriginalId, int volume) {
+        if (!TextUtils.equals(routeOriginalId, mSelectedRouteId)) {
             return;
         }
         mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
     }
 
     @Override
-    public void setSessionVolume(long requestId, String sessionId, int volume) {
+    public void setSessionVolume(long requestId, String sessionOriginalId, int volume) {
         // Do nothing since we don't support grouping volume yet.
     }
 
     @Override
-    public void prepareReleaseSession(String sessionId) {
+    public void prepareReleaseSession(String sessionUniqueId) {
         // Do nothing since the system session persists.
     }
 
@@ -503,12 +503,13 @@
         }
 
         long pendingRequestId = mPendingSessionCreationOrTransferRequest.mRequestId;
-        if (mPendingSessionCreationOrTransferRequest.mTargetRouteId.equals(mSelectedRouteId)) {
+        if (mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId.equals(
+                mSelectedRouteId)) {
             if (DEBUG) {
                 Slog.w(
                         TAG,
                         "Session creation success to route "
-                                + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+                                + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId);
             }
             mPendingSessionCreationOrTransferRequest = null;
             mCallback.onSessionCreated(this, pendingRequestId, newSessionInfo);
@@ -520,7 +521,8 @@
                     Slog.w(
                             TAG,
                             "Session creation failed to route "
-                                    + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+                                    + mPendingSessionCreationOrTransferRequest
+                                            .mTargetOriginalRouteId);
                 }
                 mPendingSessionCreationOrTransferRequest = null;
                 mCallback.onRequestFailed(
@@ -529,7 +531,7 @@
                 Slog.w(
                         TAG,
                         "Session creation waiting state to route "
-                                + mPendingSessionCreationOrTransferRequest.mTargetRouteId);
+                                + mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId);
             }
         }
     }
@@ -541,7 +543,8 @@
         // See b/307723189 for context
         for (MediaRoute2Info btRoute : mBluetoothRouteController.getAllBluetoothRoutes()) {
             if (TextUtils.equals(
-                    btRoute.getId(), mPendingSessionCreationOrTransferRequest.mTargetRouteId)) {
+                    btRoute.getId(),
+                    mPendingSessionCreationOrTransferRequest.mTargetOriginalRouteId)) {
                 return true;
             }
         }
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 3949dfe..1a8e44b 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -1626,8 +1626,8 @@
 
         ApplicationInfo appInfo = null;
         try {
-            appInfo = mContext.getPackageManager().getApplicationInfo(
-                name.getPackageName(), 0);
+            appInfo = mContext.getPackageManager().getApplicationInfoAsUser(
+                name.getPackageName(), 0, userid);
         } catch (NameNotFoundException e) {
             // Ignore if the package doesn't exist we won't be able to bind to the service.
         }
diff --git a/services/core/java/com/android/server/pm/KillAppBlocker.java b/services/core/java/com/android/server/pm/KillAppBlocker.java
new file mode 100644
index 0000000..e2901c3
--- /dev/null
+++ b/services/core/java/com/android/server/pm/KillAppBlocker.java
@@ -0,0 +1,114 @@
+/*
+ * 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.pm;
+
+import static android.app.ActivityManager.PROCESS_STATE_NONEXISTENT;
+import static android.content.pm.PackageManager.MATCH_ALL;
+import static android.os.Process.INVALID_UID;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.IActivityManager;
+import android.app.IUidObserver;
+import android.app.UidObserver;
+import android.os.Process;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Use to monitor UIDs are really killed by the {@link IUidObserver}
+ */
+final class KillAppBlocker {
+    private static final int MAX_WAIT_TIMEOUT_MS = 1000;
+    private CountDownLatch mUidsGoneCountDownLatch = new CountDownLatch(1);
+    private List mActiveUids = new ArrayList();
+    private boolean mRegistered = false;
+
+    private final IUidObserver mUidObserver = new UidObserver() {
+        @Override
+        public void onUidGone(int uid, boolean disabled) {
+            synchronized (this) {
+                mActiveUids.remove((Integer) uid);
+
+                if (mActiveUids.size() == 0) {
+                    mUidsGoneCountDownLatch.countDown();
+                }
+            }
+        }
+    };
+
+    void register() {
+        if (!mRegistered) {
+            IActivityManager am = ActivityManager.getService();
+            if (am != null) {
+                try {
+                    am.registerUidObserver(mUidObserver, ActivityManager.UID_OBSERVER_GONE,
+                            ActivityManager.PROCESS_STATE_UNKNOWN, "pm");
+                    mRegistered = true;
+                } catch (RemoteException e) {
+                    // no-op
+                }
+            }
+        }
+    }
+
+    void unregister() {
+        if (mRegistered) {
+            IActivityManager am = ActivityManager.getService();
+            if (am != null) {
+                try {
+                    mRegistered = false;
+                    am.unregisterUidObserver(mUidObserver);
+                } catch (RemoteException e) {
+                    // no-op
+                }
+            }
+        }
+    }
+
+    void waitAppProcessGone(ActivityManagerInternal mAmi, Computer snapshot,
+            UserManagerService userManager, String packageName) {
+        if (!mRegistered) {
+            return;
+        }
+        synchronized (this) {
+            if (mAmi != null) {
+                int[] users = userManager.getUserIds();
+
+                for (int i = 0; i < users.length; i++) {
+                    final int userId = users[i];
+                    final int uid = snapshot.getPackageUidInternal(
+                            packageName, MATCH_ALL, userId, Process.SYSTEM_UID);
+                    if (uid != INVALID_UID) {
+                        if (mAmi.getUidProcessState(uid) != PROCESS_STATE_NONEXISTENT) {
+                            mActiveUids.add(uid);
+                        }
+                    }
+                }
+            }
+        }
+
+        try {
+            mUidsGoneCountDownLatch.await(MAX_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // no-op
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index dec97fb..0d1095f 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -704,7 +704,8 @@
             return false;
         }
 
-        if (isAppOptedOutOfArchiving(packageName, ps.getAppId())) {
+        if (isAppOptedOutOfArchiving(packageName,
+                    UserHandle.getUid(userId, ps.getAppId()))) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index f8fceda..679ab65 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3136,13 +3136,18 @@
     void killApplicationSync(String pkgName, @AppIdInt int appId,
             @UserIdInt int userId, String reason, int exitInfoReason) {
         ActivityManagerInternal mAmi = LocalServices.getService(ActivityManagerInternal.class);
-        if (mAmi != null) {
-            if (Thread.holdsLock(mLock)) {
-                // holds PM's lock, go back killApplication to avoid it run into watchdog reset.
-                Slog.e(TAG, "Holds PM's locker, unable kill application synchronized");
-                killApplication(pkgName, appId, userId, reason, exitInfoReason);
-            } else {
+        if (Thread.holdsLock(mLock) || mAmi == null) {
+            // holds PM's lock, go back killApplication to avoid it run into watchdog reset.
+            Slog.e(TAG, "Holds PM's lock, unable kill application synchronized");
+            killApplication(pkgName, appId, userId, reason, exitInfoReason);
+        } else {
+            KillAppBlocker blocker = new KillAppBlocker();
+            try {
+                blocker.register();
                 mAmi.killApplicationSync(pkgName, appId, userId, reason, exitInfoReason);
+                blocker.waitAppProcessGone(mAmi, snapshotComputer(), mUserManager, pkgName);
+            } finally {
+                blocker.unregister();
             }
         }
     }
@@ -4052,9 +4057,6 @@
                 return;
             }
 
-            // Log the metrics when the component state is changed.
-            PackageMetrics.reportComponentStateChanged(computer, componentStateMetricsList, userId);
-
             if (isSynchronous) {
                 flushPackageRestrictionsAsUserInternalLocked(userId);
             } else {
@@ -4074,6 +4076,10 @@
             }
         }
 
+        // Log the metrics when the component state is changed.
+        PackageMetrics.reportComponentStateChanged(snapshotComputer(), componentStateMetricsList,
+                userId);
+
         final long callingId = Binder.clearCallingIdentity();
         try {
             final Computer newSnapshot = snapshotComputer();
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index c8bcc51..e753ce8 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -926,7 +926,9 @@
         }
         Slog.i(TAG, "Enabling rollback for install of " + packageName
                 + ", session:" + session.sessionId
-                + ", rollbackDataPolicy=" + rollbackDataPolicy);
+                + ", rollbackDataPolicy=" + rollbackDataPolicy
+                + ", rollbackId:" + rollback.info.getRollbackId()
+                + ", originalSessionId:" + rollback.getOriginalSessionId());
 
         final String installerPackageName = session.getInstallerPackageName();
         if (!enableRollbackAllowed(installerPackageName, packageName)) {
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e00e813..04db3e8 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -72,7 +72,6 @@
 import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.Xml;
@@ -88,6 +87,7 @@
 import com.android.server.SystemService;
 import com.android.server.servicewatcher.CurrentUserServiceSupplier;
 import com.android.server.servicewatcher.ServiceWatcher;
+import com.android.server.utils.Slogf;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -98,7 +98,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-
+import java.util.Objects;
 
 /**
  * Manages trust agents and trust listeners.
@@ -275,6 +275,10 @@
             return KeyStoreAuthorization.getInstance();
         }
 
+        AlarmManager getAlarmManager() {
+            return mContext.getSystemService(AlarmManager.class);
+        }
+
         Looper getLooper() {
             return Looper.myLooper();
         }
@@ -293,7 +297,7 @@
         mLockPatternUtils = injector.getLockPatternUtils();
         mKeyStoreAuthorization = injector.getKeyStoreAuthorization();
         mStrongAuthTracker = new StrongAuthTracker(context, injector.getLooper());
-        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        mAlarmManager = injector.getAlarmManager();
     }
 
     @Override
@@ -362,6 +366,13 @@
     }
 
     private void scheduleTrustTimeout(boolean override, boolean isTrustableTimeout) {
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "scheduleTrustTimeout(override=%s, isTrustable=%s)",
+                    override,
+                    isTrustableTimeout);
+        }
         int shouldOverride = override ? 1 : 0;
         int trustableTimeout = isTrustableTimeout ? 1 : 0;
         mHandler.obtainMessage(MSG_SCHEDULE_TRUST_TIMEOUT, shouldOverride,
@@ -370,6 +381,13 @@
 
     private void handleScheduleTrustTimeout(boolean shouldOverride, TimeoutType timeoutType) {
         int userId = mCurrentUser;
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "handleScheduleTrustTimeout(shouldOverride=%s, timeoutType=%s)",
+                    shouldOverride,
+                    timeoutType);
+        }
         if (timeoutType == TimeoutType.TRUSTABLE) {
             // don't override the hard timeout unless biometric or knowledge factor authentication
             // occurs which isn't where this is called from. Override the idle timeout what the
@@ -383,6 +401,7 @@
 
     /* Override both the idle and hard trustable timeouts */
     private void refreshTrustableTimers(int userId) {
+        if (DEBUG) Slogf.d(TAG, "refreshTrustableTimers(userId=%s)", userId);
         handleScheduleTrustableTimeouts(userId, true /* overrideIdleTimeout */,
                 true /* overrideHardTimeout */);
     }
@@ -405,13 +424,20 @@
     }
 
     private void handleScheduleTrustedTimeout(int userId, boolean shouldOverride) {
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "handleScheduleTrustedTimeout(userId=%s, shouldOverride=%s)",
+                    userId,
+                    shouldOverride);
+        }
         long when = SystemClock.elapsedRealtime() + TRUST_TIMEOUT_IN_MILLIS;
         TrustedTimeoutAlarmListener alarm = mTrustTimeoutAlarmListenerForUser.get(userId);
 
         // Cancel existing trust timeouts for this user if needed.
         if (alarm != null) {
             if (!shouldOverride && alarm.isQueued()) {
-                if (DEBUG) Slog.d(TAG, "Found existing trust timeout alarm. Skipping.");
+                if (DEBUG) Slogf.d(TAG, "Found existing trust timeout alarm. Skipping.");
                 return;
             }
             mAlarmManager.cancel(alarm);
@@ -420,7 +446,9 @@
             mTrustTimeoutAlarmListenerForUser.put(userId, alarm);
         }
 
-        if (DEBUG) Slog.d(TAG, "\tSetting up trust timeout alarm");
+        if (DEBUG) {
+            Slogf.d(TAG, "\tSetting up trust timeout alarm triggering at elapsedRealTime=%s", when);
+        }
         alarm.setQueued(true /* isQueued */);
         mAlarmManager.setExact(
                 AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -434,6 +462,13 @@
     }
 
     private void setUpIdleTimeout(int userId, boolean overrideIdleTimeout) {
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "setUpIdleTimeout(userId=%s, overrideIdleTimeout=%s)",
+                    userId,
+                    overrideIdleTimeout);
+        }
         long when = SystemClock.elapsedRealtime() + TRUSTABLE_IDLE_TIMEOUT_IN_MILLIS;
         TrustableTimeoutAlarmListener alarm = mIdleTrustableTimeoutAlarmListenerForUser.get(userId);
         mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null);
@@ -441,7 +476,7 @@
         // Cancel existing trustable timeouts for this user if needed.
         if (alarm != null) {
             if (!overrideIdleTimeout && alarm.isQueued()) {
-                if (DEBUG) Slog.d(TAG, "Found existing trustable timeout alarm. Skipping.");
+                if (DEBUG) Slogf.d(TAG, "Found existing trustable timeout alarm. Skipping.");
                 return;
             }
             mAlarmManager.cancel(alarm);
@@ -450,7 +485,12 @@
             mIdleTrustableTimeoutAlarmListenerForUser.put(userId, alarm);
         }
 
-        if (DEBUG) Slog.d(TAG, "\tSetting up trustable idle timeout alarm");
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "\tSetting up trustable idle timeout alarm triggering at elapsedRealTime=%s",
+                    when);
+        }
         alarm.setQueued(true /* isQueued */);
         mAlarmManager.setExact(
                 AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -458,6 +498,13 @@
     }
 
     private void setUpHardTimeout(int userId, boolean overrideHardTimeout) {
+        if (DEBUG) {
+            Slogf.i(
+                    TAG,
+                    "setUpHardTimeout(userId=%s, overrideHardTimeout=%s)",
+                    userId,
+                    overrideHardTimeout);
+        }
         mContext.enforceCallingOrSelfPermission(Manifest.permission.SCHEDULE_EXACT_ALARM, null);
         TrustableTimeoutAlarmListener alarm = mTrustableTimeoutAlarmListenerForUser.get(userId);
 
@@ -472,7 +519,13 @@
             } else if (overrideHardTimeout) {
                 mAlarmManager.cancel(alarm);
             }
-            if (DEBUG) Slog.d(TAG, "\tSetting up trustable hard timeout alarm");
+            if (DEBUG) {
+                Slogf.d(
+                        TAG,
+                        "\tSetting up trustable hard timeout alarm triggering at "
+                                + "elapsedRealTime=%s",
+                        when);
+            }
             alarm.setQueued(true /* isQueued */);
             mAlarmManager.setExact(
                     AlarmManager.ELAPSED_REALTIME_WAKEUP, when, TRUST_TIMEOUT_ALARM_TAG, alarm,
@@ -503,6 +556,12 @@
         public int hashCode() {
             return component.hashCode() * 31 + userId;
         }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    "AgentInfo{label=%s, component=%s, userId=%s}", label, component, userId);
+        }
     }
 
     private void updateTrustAll() {
@@ -532,6 +591,15 @@
             int flags,
             boolean isFromUnlock,
             @Nullable AndroidFuture<GrantTrustResult> resultCallback) {
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "updateTrust(userId=%s, flags=%s, isFromUnlock=%s, resultCallbackPresent=%s)",
+                    userId,
+                    flags,
+                    isFromUnlock,
+                    Objects.isNull(resultCallback));
+        }
         boolean managed = aggregateIsTrustManaged(userId);
         dispatchOnTrustManagedChanged(managed, userId);
         if (mStrongAuthTracker.isTrustAllowedForUser(userId)
@@ -559,27 +627,50 @@
                     (flags & TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) != 0);
             boolean canMoveToTrusted =
                     alreadyUnlocked || isFromUnlock || renewingTrust || isAutomotive();
-            boolean upgradingTrustForCurrentUser = (userId == mCurrentUser);
+            boolean updatingTrustForCurrentUser = (userId == mCurrentUser);
+
+            if (DEBUG) {
+                Slogf.d(
+                        TAG,
+                        "updateTrust: alreadyUnlocked=%s, wasTrusted=%s, wasTrustable=%s, "
+                                + "renewingTrust=%s, canMoveToTrusted=%s, "
+                                + "updatingTrustForCurrentUser=%s",
+                        alreadyUnlocked,
+                        wasTrusted,
+                        wasTrustable,
+                        renewingTrust,
+                        canMoveToTrusted,
+                        updatingTrustForCurrentUser);
+            }
 
             if (trustedByAtLeastOneAgent && wasTrusted) {
                 // no change
                 return;
-            } else if (trustedByAtLeastOneAgent && canMoveToTrusted
-                    && upgradingTrustForCurrentUser) {
+            } else if (trustedByAtLeastOneAgent
+                    && canMoveToTrusted
+                    && updatingTrustForCurrentUser) {
                 pendingTrustState = TrustState.TRUSTED;
-            } else if (trustableByAtLeastOneAgent && (wasTrusted || wasTrustable)
-                    && upgradingTrustForCurrentUser) {
+            } else if (trustableByAtLeastOneAgent
+                    && (wasTrusted || wasTrustable)
+                    && updatingTrustForCurrentUser) {
                 pendingTrustState = TrustState.TRUSTABLE;
             } else {
                 pendingTrustState = TrustState.UNTRUSTED;
             }
+            if (DEBUG) Slogf.d(TAG, "updateTrust: pendingTrustState=%s", pendingTrustState);
 
             mUserTrustState.put(userId, pendingTrustState);
         }
-        if (DEBUG) Slog.d(TAG, "pendingTrustState: " + pendingTrustState);
 
         boolean isNowTrusted = pendingTrustState == TrustState.TRUSTED;
         boolean newlyUnlocked = !alreadyUnlocked && isNowTrusted;
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "updateTrust: isNowTrusted=%s, newlyUnlocked=%s",
+                    isNowTrusted,
+                    newlyUnlocked);
+        }
         maybeActiveUnlockRunningChanged(userId);
         dispatchOnTrustChanged(
                 isNowTrusted, newlyUnlocked, userId, flags, getTrustGrantedMessages(userId));
@@ -598,13 +689,13 @@
         boolean shouldSendCallback = newlyUnlocked;
         if (shouldSendCallback) {
             if (resultCallback != null) {
-                if (DEBUG) Slog.d(TAG, "calling back with UNLOCKED_BY_GRANT");
+                if (DEBUG) Slogf.d(TAG, "calling back with UNLOCKED_BY_GRANT");
                 resultCallback.complete(new GrantTrustResult(STATUS_UNLOCKED_BY_GRANT));
             }
         }
 
         if ((wasTrusted || wasTrustable) && pendingTrustState == TrustState.UNTRUSTED) {
-            if (DEBUG) Slog.d(TAG, "Trust was revoked, destroy trustable alarms");
+            if (DEBUG) Slogf.d(TAG, "Trust was revoked, destroy trustable alarms");
             cancelBothTrustableAlarms(userId);
         }
     }
@@ -650,7 +741,7 @@
         try {
             WindowManagerGlobal.getWindowManagerService().lockNow(null);
         } catch (RemoteException e) {
-            Slog.e(TAG, "Error locking screen when called from trust agent");
+            Slogf.e(TAG, "Error locking screen when called from trust agent");
         }
     }
 
@@ -659,8 +750,9 @@
     }
 
     void refreshAgentList(int userIdOrAll) {
-        if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")");
+        if (DEBUG) Slogf.d(TAG, "refreshAgentList(userIdOrAll=%s)", userIdOrAll);
         if (!mTrustAgentsCanRun) {
+            if (DEBUG) Slogf.d(TAG, "Did not refresh agent list because agents cannot run.");
             return;
         }
         if (userIdOrAll != UserHandle.USER_ALL && userIdOrAll < UserHandle.USER_SYSTEM) {
@@ -686,18 +778,30 @@
             if (userInfo == null || userInfo.partial || !userInfo.isEnabled()
                     || userInfo.guestToRemove) continue;
             if (!userInfo.supportsSwitchToByUser()) {
-                if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                        + ": switchToByUser=false");
+                if (DEBUG) {
+                    Slogf.d(
+                            TAG,
+                            "refreshAgentList: skipping user %s: switchToByUser=false",
+                            userInfo.id);
+                }
                 continue;
             }
             if (!mActivityManager.isUserRunning(userInfo.id)) {
-                if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                        + ": user not started");
+                if (DEBUG) {
+                    Slogf.d(
+                            TAG,
+                            "refreshAgentList: skipping user %s: user not started",
+                            userInfo.id);
+                }
                 continue;
             }
             if (!lockPatternUtils.isSecure(userInfo.id)) {
-                if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                        + ": no secure credential");
+                if (DEBUG) {
+                    Slogf.d(
+                            TAG,
+                            "refreshAgentList: skipping user %s: no secure credential",
+                            userInfo.id);
+                }
                 continue;
             }
 
@@ -708,8 +812,12 @@
 
             List<ComponentName> enabledAgents = lockPatternUtils.getEnabledTrustAgents(userInfo.id);
             if (enabledAgents.isEmpty()) {
-                if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                        + ": no agents enabled by user");
+                if (DEBUG) {
+                    Slogf.d(
+                            TAG,
+                            "refreshAgentList: skipping user %s: no agents enabled by user",
+                            userInfo.id);
+                }
                 continue;
             }
             List<ResolveInfo> resolveInfos = resolveAllowedTrustAgents(pm, userInfo.id);
@@ -717,9 +825,13 @@
                 ComponentName name = getComponentName(resolveInfo);
 
                 if (!enabledAgents.contains(name)) {
-                    if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping "
-                            + name.flattenToShortString() + " u"+ userInfo.id
-                            + ": not enabled by user");
+                    if (DEBUG) {
+                        Slogf.d(
+                                TAG,
+                                "refreshAgentList: skipping %s u%s: not enabled by user",
+                                name.flattenToShortString(),
+                                userInfo.id);
+                    }
                     continue;
                 }
                 if (disableTrustAgents) {
@@ -727,9 +839,13 @@
                             dpm.getTrustAgentConfiguration(null /* admin */, name, userInfo.id);
                     // Disable agent if no features are enabled.
                     if (config == null || config.isEmpty()) {
-                        if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping "
-                                + name.flattenToShortString() + " u"+ userInfo.id
-                                + ": not allowed by DPM");
+                        if (DEBUG) {
+                            Slogf.d(
+                                    TAG,
+                                    "refreshAgentList: skipping %s u%s: not allowed by DPM",
+                                    name.flattenToShortString(),
+                                    userInfo.id);
+                        }
                         continue;
                     }
                 }
@@ -752,15 +868,26 @@
                 }
 
                 if (directUnlock) {
-                    if (DEBUG) Slog.d(TAG, "refreshAgentList: trustagent " + name
-                            + "of user " + userInfo.id + "can unlock user profile.");
+                    if (DEBUG) {
+                        Slogf.d(
+                                TAG,
+                                "refreshAgentList: trustagent %s of user %s can unlock user "
+                                        + "profile.",
+                                name,
+                                userInfo.id);
+                    }
                 }
 
                 if (!mUserManager.isUserUnlockingOrUnlocked(userInfo.id)
                         && !directUnlock) {
-                    if (DEBUG) Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                            + "'s trust agent " + name + ": FBE still locked and "
-                            + " the agent cannot unlock user profile.");
+                    if (DEBUG) {
+                        Slogf.d(
+                                TAG,
+                                "refreshAgentList: skipping user %s's trust agent %s: FBE still "
+                                        + "locked and the agent cannot unlock user profile.",
+                                userInfo.id,
+                                name);
+                    }
                     continue;
                 }
 
@@ -769,11 +896,16 @@
                     if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) {
                         if (flag != StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
                             || !directUnlock) {
-                            if (DEBUG)
-                                Slog.d(TAG, "refreshAgentList: skipping user " + userInfo.id
-                                    + ": prevented by StrongAuthTracker = 0x"
-                                    + Integer.toHexString(mStrongAuthTracker.getStrongAuthForUser(
-                                    userInfo.id)));
+                            if (DEBUG) {
+                                Slogf.d(
+                                        TAG,
+                                        "refreshAgentList: skipping user %s: prevented by "
+                                                + "StrongAuthTracker = 0x%s",
+                                        userInfo.id,
+                                        Integer.toHexString(
+                                                mStrongAuthTracker.getStrongAuthForUser(
+                                                        userInfo.id)));
+                            }
                             continue;
                         }
                     }
@@ -804,6 +936,15 @@
             }
         }
 
+        if (DEBUG) {
+            Slogf.d(
+                    TAG,
+                    "refreshAgentList: userInfos=%s, obsoleteAgents=%s, trustMayHaveChanged=%s",
+                    userInfos,
+                    obsoleteAgents,
+                    trustMayHaveChanged);
+        }
+
         if (trustMayHaveChanged) {
             if (userIdOrAll == UserHandle.USER_ALL) {
                 updateTrustAll();
@@ -1044,7 +1185,7 @@
             parser = resolveInfo.serviceInfo.loadXmlMetaData(pm,
                     TrustAgentService.TRUST_AGENT_META_DATA);
             if (parser == null) {
-                Slog.w(TAG, "Can't find " + TrustAgentService.TRUST_AGENT_META_DATA + " meta-data");
+                Slogf.w(TAG, "Can't find %s meta-data", TrustAgentService.TRUST_AGENT_META_DATA);
                 return null;
             }
             Resources res = pm.getResourcesForApplication(resolveInfo.serviceInfo.applicationInfo);
@@ -1056,7 +1197,7 @@
             }
             String nodeName = parser.getName();
             if (!"trust-agent".equals(nodeName)) {
-                Slog.w(TAG, "Meta-data does not start with trust-agent tag");
+                Slogf.w(TAG, "Meta-data does not start with trust-agent tag");
                 return null;
             }
             TypedArray sa = res
@@ -1075,7 +1216,11 @@
             if (parser != null) parser.close();
         }
         if (caughtException != null) {
-            Slog.w(TAG, "Error parsing : " + resolveInfo.serviceInfo.packageName, caughtException);
+            Slogf.w(
+                    TAG,
+                    caughtException,
+                    "Error parsing : %s",
+                    resolveInfo.serviceInfo.packageName);
             return null;
         }
         if (cn == null) {
@@ -1242,13 +1387,18 @@
     // Agent dispatch and aggregation
 
     private boolean aggregateIsTrusted(int userId) {
+        if (DEBUG) Slogf.d(TAG, "aggregateIsTrusted(userId=%s)", userId);
         if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+            if (DEBUG) {
+                Slogf.d(TAG, "not trusted because trust not allowed for userId=%s", userId);
+            }
             return false;
         }
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo info = mActiveAgents.valueAt(i);
             if (info.userId == userId) {
                 if (info.agent.isTrusted()) {
+                    if (DEBUG) Slogf.d(TAG, "trusted by %s", info);
                     return true;
                 }
             }
@@ -1257,13 +1407,18 @@
     }
 
     private boolean aggregateIsTrustable(int userId) {
+        if (DEBUG) Slogf.d(TAG, "aggregateIsTrustable(userId=%s)", userId);
         if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+            if (DEBUG) {
+                Slogf.d(TAG, "not trustable because trust not allowed for userId=%s", userId);
+            }
             return false;
         }
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo info = mActiveAgents.valueAt(i);
             if (info.userId == userId) {
                 if (info.agent.isTrustable()) {
+                    if (DEBUG) Slogf.d(TAG, "trustable by %s", info);
                     return true;
                 }
             }
@@ -1328,20 +1483,31 @@
 
     private boolean aggregateIsTrustManaged(int userId) {
         if (!mStrongAuthTracker.isTrustAllowedForUser(userId)) {
+            if (DEBUG) {
+                Slogf.d(
+                        TAG,
+                        "trust not managed due to trust not being allowed for userId=%s",
+                        userId);
+            }
             return false;
         }
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo info = mActiveAgents.valueAt(i);
             if (info.userId == userId) {
                 if (info.agent.isManagingTrust()) {
+                    if (DEBUG) Slogf.d(TAG, "trust managed for userId=%s", userId);
                     return true;
                 }
             }
         }
+        if (DEBUG) Slogf.d(TAG, "trust not managed for userId=%s", userId);
         return false;
     }
 
     private void dispatchUnlockAttempt(boolean successful, int userId) {
+        if (DEBUG) {
+            Slogf.d(TAG, "dispatchUnlockAttempt(successful=%s, userId=%s)", successful, userId);
+        }
         if (successful) {
             mStrongAuthTracker.allowTrustFromUnlock(userId);
             // Allow the presence of trust on a successful unlock attempt to extend unlock
@@ -1359,8 +1525,11 @@
 
     private void dispatchUserRequestedUnlock(int userId, boolean dismissKeyguard) {
         if (DEBUG) {
-            Slog.d(TAG, "dispatchUserRequestedUnlock(user=" + userId + ", dismissKeyguard="
-                    + dismissKeyguard + ")");
+            Slogf.d(
+                    TAG,
+                    "dispatchUserRequestedUnlock(user=%s, dismissKeyguard=%s)",
+                    userId,
+                    dismissKeyguard);
         }
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo info = mActiveAgents.valueAt(i);
@@ -1372,7 +1541,7 @@
 
     private void dispatchUserMayRequestUnlock(int userId) {
         if (DEBUG) {
-            Slog.d(TAG, "dispatchUserMayRequestUnlock(user=" + userId + ")");
+            Slogf.d(TAG, "dispatchUserMayRequestUnlock(user=%s)", userId);
         }
         for (int i = 0; i < mActiveAgents.size(); i++) {
             AgentInfo info = mActiveAgents.valueAt(i);
@@ -1405,9 +1574,9 @@
         try {
             listener.onIsActiveUnlockRunningChanged(isRunning, userId);
         } catch (DeadObjectException e) {
-            Slog.d(TAG, "TrustListener dead while trying to notify Active Unlock running state");
+            Slogf.d(TAG, "TrustListener dead while trying to notify Active Unlock running state");
         } catch (RemoteException e) {
-            Slog.e(TAG, "Exception while notifying TrustListener.", e);
+            Slogf.e(TAG, "Exception while notifying TrustListener.", e);
         }
     }
 
@@ -1445,11 +1614,11 @@
                 mTrustListeners.get(i).onTrustChanged(
                         enabled, newlyUnlocked, userId, flags, trustGrantedMessages);
             } catch (DeadObjectException e) {
-                Slog.d(TAG, "Removing dead TrustListener.");
+                Slogf.d(TAG, "Removing dead TrustListener.");
                 mTrustListeners.remove(i);
                 i--;
             } catch (RemoteException e) {
-                Slog.e(TAG, "Exception while notifying TrustListener.", e);
+                Slogf.e(TAG, "Exception while notifying TrustListener.", e);
             }
         }
     }
@@ -1462,11 +1631,11 @@
             try {
                 mTrustListeners.get(i).onEnabledTrustAgentsChanged(userId);
             } catch (DeadObjectException e) {
-                Slog.d(TAG, "Removing dead TrustListener.");
+                Slogf.d(TAG, "Removing dead TrustListener.");
                 mTrustListeners.remove(i);
                 i--;
             } catch (RemoteException e) {
-                Slog.e(TAG, "Exception while notifying TrustListener.", e);
+                Slogf.e(TAG, "Exception while notifying TrustListener.", e);
             }
         }
     }
@@ -1479,11 +1648,11 @@
             try {
                 mTrustListeners.get(i).onTrustManagedChanged(managed, userId);
             } catch (DeadObjectException e) {
-                Slog.d(TAG, "Removing dead TrustListener.");
+                Slogf.d(TAG, "Removing dead TrustListener.");
                 mTrustListeners.remove(i);
                 i--;
             } catch (RemoteException e) {
-                Slog.e(TAG, "Exception while notifying TrustListener.", e);
+                Slogf.e(TAG, "Exception while notifying TrustListener.", e);
             }
         }
     }
@@ -1496,11 +1665,11 @@
             try {
                 mTrustListeners.get(i).onTrustError(message);
             } catch (DeadObjectException e) {
-                Slog.d(TAG, "Removing dead TrustListener.");
+                Slogf.d(TAG, "Removing dead TrustListener.");
                 mTrustListeners.remove(i);
                 i--;
             } catch (RemoteException e) {
-                Slog.e(TAG, "Exception while notifying TrustListener.", e);
+                Slogf.e(TAG, "Exception while notifying TrustListener.", e);
             }
         }
     }
@@ -1535,7 +1704,7 @@
                     && mFingerprintManager.hasEnrolledTemplates(userId)
                     && isWeakOrConvenienceSensor(
                             mFingerprintManager.getSensorProperties().get(0))) {
-                Slog.i(TAG, "User is unlockable by non-strong fingerprint auth");
+                Slogf.i(TAG, "User is unlockable by non-strong fingerprint auth");
                 return true;
             }
 
@@ -1543,7 +1712,7 @@
                     && (disabledFeatures & DevicePolicyManager.KEYGUARD_DISABLE_FACE) == 0
                     && mFaceManager.hasEnrolledTemplates(userId)
                     && isWeakOrConvenienceSensor(mFaceManager.getSensorProperties().get(0))) {
-                Slog.i(TAG, "User is unlockable by non-strong face auth");
+                Slogf.i(TAG, "User is unlockable by non-strong face auth");
                 return true;
             }
         }
@@ -1551,7 +1720,7 @@
         // Check whether it's possible for the device to be actively unlocked by a trust agent.
         if (getUserTrustStateInner(userId) == TrustState.TRUSTABLE
                 || (isAutomotive() && isTrustUsuallyManagedInternal(userId))) {
-            Slog.i(TAG, "User is unlockable by trust agent");
+            Slogf.i(TAG, "User is unlockable by trust agent");
             return true;
         }
 
@@ -1595,6 +1764,13 @@
     private final IBinder mService = new ITrustManager.Stub() {
         @Override
         public void reportUnlockAttempt(boolean authenticated, int userId) throws RemoteException {
+            if (DEBUG) {
+                Slogf.d(
+                        TAG,
+                        "reportUnlockAttempt(authenticated=%s, userId=%s)",
+                        authenticated,
+                        userId);
+            }
             enforceReportPermission();
             mHandler.obtainMessage(MSG_DISPATCH_UNLOCK_ATTEMPT, authenticated ? 1 : 0, userId)
                     .sendToTarget();
@@ -1611,7 +1787,8 @@
         @Override
         public void reportUserMayRequestUnlock(int userId) throws RemoteException {
             enforceReportPermission();
-            mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /*arg2=*/ 0).sendToTarget();
+            mHandler.obtainMessage(MSG_USER_MAY_REQUEST_UNLOCK, userId, /* arg2= */ 0)
+                    .sendToTarget();
         }
 
         @Override
@@ -1932,6 +2109,7 @@
         return new Handler(looper) {
             @Override
             public void handleMessage(Message msg) {
+                if (DEBUG) Slogf.d(TAG, "handler: %s", msg.what);
                 switch (msg.what) {
                     case MSG_REGISTER_LISTENER:
                         addListener((ITrustListener) msg.obj);
@@ -2002,8 +2180,24 @@
                         handleScheduleTrustTimeout(shouldOverride, timeoutType);
                         break;
                     case MSG_REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH:
+                        if (DEBUG) {
+                            Slogf.d(TAG, "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH userId=%s", msg.arg1);
+                        }
                         TrustableTimeoutAlarmListener trustableAlarm =
                                 mTrustableTimeoutAlarmListenerForUser.get(msg.arg1);
+                        if (DEBUG) {
+                            if (trustableAlarm != null) {
+                                Slogf.d(
+                                        TAG,
+                                        "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH trustable alarm "
+                                                + "isQueued=%s",
+                                        trustableAlarm.mIsQueued);
+                            } else {
+                                Slogf.d(
+                                        TAG,
+                                        "REFRESH_TRUSTABLE_TIMERS_AFTER_AUTH no trustable alarm");
+                            }
+                        }
                         if (trustableAlarm != null && trustableAlarm.isQueued()) {
                             refreshTrustableTimers(msg.arg1);
                         }
@@ -2194,7 +2388,7 @@
             handleAlarm();
             // Only fire if trust can unlock.
             if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) {
-                if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout");
+                if (DEBUG) Slogf.d(TAG, "Revoking all trust because of trust timeout");
                 mLockPatternUtils.requireStrongAuth(
                         mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId);
             }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0048849..2d2a88a86 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3964,7 +3964,7 @@
         }
 
         if (isCurrentVisible) {
-            if (isNextNotYetVisible || delayRemoval) {
+            if (isNextNotYetVisible || delayRemoval || (next != null && isInTransition())) {
                 // Add this activity to the list of stopping activities. It will be processed and
                 // destroyed when the next activity reports idle.
                 addToStopping(false /* scheduleIdle */, false /* idleDelayed */,
@@ -7644,6 +7644,8 @@
             // This could only happen when the window is removed from hierarchy. So do not keep its
             // reference anymore.
             mStartingWindow = null;
+            mStartingData = null;
+            mStartingSurface = null;
         }
         if (mChildren.size() == 0 && mVisibleSetFromTransferredStartingWindow) {
             // We set the visible state to true for the token from a transferred starting
@@ -9259,6 +9261,19 @@
             @NonNull CompatDisplayInsets compatDisplayInsets) {
         final Configuration resolvedConfig = getResolvedOverrideConfiguration();
         final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
+        final Insets insets;
+        if (mResolveConfigHint.mUseOverrideInsetsForConfig) {
+            // TODO(b/343197837): Add test to verify SCM behaviour with new bound configuration
+            // Insets are decoupled from configuration by default from V+, use legacy
+            // compatibility behaviour for apps targeting SDK earlier than 35
+            // (see applySizeOverrideIfNeeded).
+            insets = Insets.of(mDisplayContent.getDisplayPolicy()
+                    .getDecorInsetsInfo(mDisplayContent.mDisplayFrames.mRotation,
+                            mDisplayContent.mDisplayFrames.mWidth,
+                            mDisplayContent.mDisplayFrames.mHeight).mOverrideNonDecorInsets);
+        } else {
+            insets = Insets.NONE;
+        }
 
         // When an activity needs to be letterboxed because of fixed orientation, use fixed
         // orientation bounds (stored in resolved bounds) instead of parent bounds since the
@@ -9269,9 +9284,12 @@
         final Rect containerBounds = useResolvedBounds
                 ? new Rect(resolvedBounds)
                 : newParentConfiguration.windowConfiguration.getBounds();
+        final Rect parentAppBounds =
+                newParentConfiguration.windowConfiguration.getAppBounds();
+        parentAppBounds.inset(insets);
         final Rect containerAppBounds = useResolvedBounds
                 ? new Rect(resolvedConfig.windowConfiguration.getAppBounds())
-                : newParentConfiguration.windowConfiguration.getAppBounds();
+                : parentAppBounds;
 
         final int requestedOrientation = getRequestedConfigurationOrientation();
         final boolean orientationRequested = requestedOrientation != ORIENTATION_UNDEFINED;
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 207707e..ac2c886 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -28,6 +28,7 @@
 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
 import static android.os.Process.INVALID_PID;
 import static android.os.Process.INVALID_UID;
+import static android.os.Process.ROOT_UID;
 import static android.os.Process.SYSTEM_UID;
 import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER;
 
@@ -385,6 +386,10 @@
                     return BackgroundStartPrivileges.NONE;
                 case MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
                     // no explicit choice by the app - let us decide what to do
+                    if (callingUid == ROOT_UID || callingUid == SYSTEM_UID) {
+                        // root and system must always opt in explicitly
+                        return BackgroundStartPrivileges.NONE;
+                    }
                     if (callingPackage != null) {
                         // determine based on the calling/creating package
                         boolean changeEnabled = CompatChanges.isChangeEnabled(
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9dc9ad4..6c48e95 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1931,6 +1931,9 @@
             if (td.getSystemBarsAppearance() == 0) {
                 td.setSystemBarsAppearance(atd.getSystemBarsAppearance());
             }
+            if (td.getTopOpaqueSystemBarsAppearance() == 0 && r.fillsParent()) {
+                td.setTopOpaqueSystemBarsAppearance(atd.getSystemBarsAppearance());
+            }
             if (td.getNavigationBarColor() == 0) {
                 td.setNavigationBarColor(atd.getNavigationBarColor());
                 td.setEnsureNavigationBarContrastWhenTransparent(
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d4bbe84..61022cc 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1346,10 +1346,13 @@
 
             // In a multi-resumed environment, like in a freeform device, the top
             // activity can be resumed, but it might not be the focused app.
-            // Set focused app when top activity is resumed
-            if (taskDisplayArea.inMultiWindowMode() && taskDisplayArea.mDisplayContent != null
-                    && taskDisplayArea.mDisplayContent.mFocusedApp != next) {
-                taskDisplayArea.mDisplayContent.setFocusedApp(next);
+            // Set focused app when top activity is resumed. However, we shouldn't do it for a
+            // same task because it can break focused state. (e.g. activity embedding)
+            if (taskDisplayArea.inMultiWindowMode() && taskDisplayArea.mDisplayContent != null) {
+                final ActivityRecord focusedApp = taskDisplayArea.mDisplayContent.mFocusedApp;
+                if (focusedApp == null || focusedApp.getTask() != next.getTask()) {
+                    taskDisplayArea.mDisplayContent.setFocusedApp(next);
+                }
             }
             ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Top activity "
                     + "resumed %s", next);
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 0bf1c88..a00b6fc4 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1525,7 +1525,7 @@
             InputChannel outInputChannel, InsetsState outInsetsState,
             InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,
             float[] outSizeCompatScale) {
-        outActiveControls.set(null);
+        outActiveControls.set(null, false /* copyControls */);
         int[] appOp = new int[1];
         final boolean isRoundedCornerOverlay = (attrs.privateFlags
                 & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
@@ -1927,7 +1927,7 @@
             displayContent.getInsetsStateController().updateAboveInsetsState(
                     false /* notifyInsetsChanged */);
 
-            outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
+            win.fillInsetsState(outInsetsState, true /* copySources */);
             getInsetsSourceControls(win, outActiveControls);
 
             if (win.mLayoutAttached) {
@@ -2317,7 +2317,7 @@
             InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls,
             Bundle outBundle, WindowRelayoutResult outRelayoutResult) {
         if (outActiveControls != null) {
-            outActiveControls.set(null);
+            outActiveControls.set(null, false /* copyControls */);
         }
         int result = 0;
         boolean configChanged = false;
@@ -2680,7 +2680,7 @@
             }
 
             if (outInsetsState != null) {
-                outInsetsState.set(win.getCompatInsetsState(), true /* copySources */);
+                win.fillInsetsState(outInsetsState, true /* copySources */);
             }
 
             ProtoLog.v(WM_DEBUG_FOCUS, "Relayout of %s: focusMayChange=%b",
@@ -2743,25 +2743,14 @@
     }
 
     private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) {
-        final InsetsSourceControl[] controls =
-                win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win);
-        if (controls != null) {
-            final int length = controls.length;
-            final InsetsSourceControl[] outControls = new InsetsSourceControl[length];
-            for (int i = 0; i < length; i++) {
-                // We will leave the critical section before returning the leash to the client,
-                // so we need to copy the leash to prevent others release the one that we are
-                // about to return.
-                if (controls[i] != null) {
-                    // This source control is an extra copy if the client is not local. By setting
-                    // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of
-                    // SurfaceControl.writeToParcel.
-                    outControls[i] = new InsetsSourceControl(controls[i]);
-                    outControls[i].setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE);
-                }
-            }
-            outArray.set(outControls);
-        }
+        // We will leave the critical section before returning the leash to the client,
+        // so we need to copy the leash to prevent others release the one that we are
+        // about to return.
+        win.fillInsetsSourceControls(outArray, true /* copyControls */);
+        // This source control is an extra copy if the client is not local. By setting
+        // PARCELABLE_WRITE_RETURN_VALUE, the leash will be released at the end of
+        // SurfaceControl.writeToParcel.
+        outArray.setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE);
     }
 
     private void tryStartExitingAnimation(WindowState win, WindowStateAnimator winAnimator) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 6953c60..d1efc27 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -434,7 +434,10 @@
     /** @see #isLastConfigReportedToClient() */
     private boolean mLastConfigReportedToClient;
 
-    // TODO(b/339380439): Ensure to use the same object for IWindowSession#relayout
+    private final ClientWindowFrames mLastReportedFrames = new ClientWindowFrames();
+
+    private final InsetsState mLastReportedInsetsState = new InsetsState();
+
     private final InsetsSourceControl.Array mLastReportedActiveControls =
             new InsetsSourceControl.Array();
 
@@ -495,8 +498,6 @@
 
     private final WindowFrames mWindowFrames = new WindowFrames();
 
-    private final ClientWindowFrames mClientWindowFrames = new ClientWindowFrames();
-
     /**
      * List of rects where system gestures should be ignored.
      *
@@ -3650,8 +3651,10 @@
                 outFrames.attachedFrame.scale(mInvGlobalScale);
             }
         }
-
         outFrames.compatScale = getCompatScaleForClient();
+        if (mLastReportedFrames != outFrames) {
+            mLastReportedFrames.setTo(outFrames);
+        }
 
         // Note: in the cases where the window is tied to an activity, we should not send a
         // configuration update when the window has requested to be hidden. Doing so can lead to
@@ -3678,6 +3681,25 @@
         mLastConfigReportedToClient = true;
     }
 
+    void fillInsetsState(@NonNull InsetsState outInsetsState, boolean copySources) {
+        outInsetsState.set(getCompatInsetsState(), copySources);
+        if (outInsetsState != mLastReportedInsetsState) {
+            // No need to copy for the recorded.
+            mLastReportedInsetsState.set(outInsetsState, false /* copySources */);
+        }
+    }
+
+    void fillInsetsSourceControls(@NonNull InsetsSourceControl.Array outArray,
+            boolean copyControls) {
+        final InsetsSourceControl[] controls =
+                getDisplayContent().getInsetsStateController().getControlsForDispatch(this);
+        outArray.set(controls, copyControls);
+        if (outArray != mLastReportedActiveControls) {
+            // No need to copy for the recorded.
+            mLastReportedActiveControls.setTo(outArray, false /* copyControls */);
+        }
+    }
+
     void reportResized() {
         // If the activity is scheduled to relaunch, skip sending the resized to ViewRootImpl now
         // since it will be destroyed anyway. This also prevents the client from receiving
@@ -3712,9 +3734,10 @@
 
         final int prevRotation = mLastReportedConfiguration
                 .getMergedConfiguration().windowConfiguration.getRotation();
-        fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,
+        fillClientWindowFramesAndConfiguration(mLastReportedFrames, mLastReportedConfiguration,
                 mLastReportedActivityWindowInfo, true /* useLatestConfig */,
                 false /* relayoutVisible */);
+        fillInsetsState(mLastReportedInsetsState, false /* copySources */);
         final boolean syncRedraw = shouldSendRedrawForSync();
         final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers();
         final boolean reportDraw = syncRedraw || drawPending;
@@ -3734,8 +3757,8 @@
 
         if (Flags.bundleClientTransactionFlag()) {
             getProcess().scheduleClientTransactionItem(
-                    WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw,
-                            mLastReportedConfiguration, getCompatInsetsState(), forceRelayout,
+                    WindowStateResizeItem.obtain(mClient, mLastReportedFrames, reportDraw,
+                            mLastReportedConfiguration, mLastReportedInsetsState, forceRelayout,
                             alwaysConsumeSystemBars, displayId,
                             syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
                             mLastReportedActivityWindowInfo));
@@ -3743,8 +3766,8 @@
         } else {
             // TODO(b/301870955): cleanup after launch
             try {
-                mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
-                        getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
+                mClient.resized(mLastReportedFrames, reportDraw, mLastReportedConfiguration,
+                        mLastReportedInsetsState, forceRelayout, alwaysConsumeSystemBars, displayId,
                         syncWithBuffers ? mSyncSeqId : -1, isDragResizing,
                         mLastReportedActivityWindowInfo);
                 onResizePostDispatched(drawPending, prevRotation, displayId);
@@ -3817,16 +3840,14 @@
         if (mRemoved) {
             return;
         }
-        final InsetsStateController stateController =
-                getDisplayContent().getInsetsStateController();
-        final InsetsState insetsState = getCompatInsetsState();
-        mLastReportedActiveControls.set(stateController.getControlsForDispatch(this));
+        fillInsetsState(mLastReportedInsetsState, false /* copySources */);
+        fillInsetsSourceControls(mLastReportedActiveControls, false /* copyControls */);
         if (Flags.insetsControlChangedItem()) {
             getProcess().scheduleClientTransactionItem(WindowStateInsetsControlChangeItem.obtain(
-                    mClient, insetsState, mLastReportedActiveControls));
+                    mClient, mLastReportedInsetsState, mLastReportedActiveControls));
         } else {
             try {
-                mClient.insetsControlChanged(insetsState, mLastReportedActiveControls);
+                mClient.insetsControlChanged(mLastReportedInsetsState, mLastReportedActiveControls);
             } catch (RemoteException e) {
                 Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
             }
diff --git a/services/core/jni/BroadcastRadio/convert.cpp b/services/core/jni/BroadcastRadio/convert.cpp
index ddbc535..e42f7f8 100644
--- a/services/core/jni/BroadcastRadio/convert.cpp
+++ b/services/core/jni/BroadcastRadio/convert.cpp
@@ -433,7 +433,7 @@
                 gjni.AmBandDescriptor.clazz, gjni.AmBandDescriptor.cstor,
                 region, config.type, config.lowerLimit, config.upperLimit, spacing, am.stereo));
     } else {
-        ALOGE("Unsupported band type: %d", config.type);
+        ALOGE("Unsupported band type: %d", static_cast<int>(config.type));
         return nullptr;
     }
 }
@@ -451,7 +451,7 @@
         return make_javaref(env, env->NewObject(
                 gjni.AmBandConfig.clazz, gjni.AmBandConfig.cstor, descriptor.get()));
     } else {
-        ALOGE("Unsupported band type: %d", config.type);
+        ALOGE("Unsupported band type: %d", static_cast<int>(config.type));
         return nullptr;
     }
 }
@@ -539,9 +539,9 @@
                         item.clockValue.timezoneOffsetInMinutes);
                 break;
             default:
-                ALOGW("invalid metadata type %d", item.type);
+                ALOGW("invalid metadata type %d", static_cast<int>(item.type));
         }
-        ALOGE_IF(status != 0, "Failed inserting metadata %d (of type %d)", key, item.type);
+        ALOGE_IF(status != 0, "Failed inserting metadata %d (of type %d)", key, static_cast<int>(item.type));
     }
 
     return jMetadata;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index 7483b43..c7fd979 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -202,9 +202,9 @@
     private String getDefaultSmsPackage() {
         //TODO(b/319449037): Unflag the following change.
         if (Flags.defaultSmsPersonalAppSuspensionFixEnabled()) {
-            return SmsApplication.getDefaultSmsApplicationAsUser(
-                            mContext, /*updateIfNeeded=*/ false, mContext.getUser())
-                    .getPackageName();
+            ComponentName defaultSmsApp = SmsApplication.getDefaultSmsApplicationAsUser(
+                    mContext, /*updateIfNeeded=*/ false, mContext.getUser());
+            return defaultSmsApp != null ? defaultSmsApp.getPackageName() : null;
         } else {
             return Telephony.Sms.getDefaultSmsPackage(mContext);
         }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 927df8b..cfe4e17 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -761,9 +761,6 @@
         }
     }
 
-    private static final long BINDER_CALLBACK_THROTTLE_MS = 10_100L;
-    private long mBinderCallbackLast = -1;
-
     private void run() {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         try {
@@ -968,14 +965,6 @@
         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/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
index b0c7073..3bdcd9b 100644
--- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt
@@ -18,6 +18,7 @@
 
 import android.app.AppOpsManager
 import android.companion.virtual.VirtualDeviceManager
+import android.os.Binder
 import android.os.Handler
 import android.os.UserHandle
 import android.permission.PermissionManager
@@ -250,7 +251,9 @@
         ) {
             Slog.w(
                 LOG_TAG,
-                "Cannot set UID mode for runtime permission app op, uid = $uid," +
+                "Cannot set UID mode for runtime permission app op, " +
+                    " callingUid = ${Binder.getCallingUid()}, " +
+                    " uid = $uid," +
                     " code = ${AppOpsManager.opToName(code)}," +
                     " mode = ${AppOpsManager.modeToName(mode)}",
                 RuntimeException()
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
index 8db896b..bba4c8d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayBrightnessStateTest.java
@@ -58,6 +58,7 @@
                 .setShouldUseAutoBrightness(shouldUseAutoBrightness)
                 .setShouldUpdateScreenBrightnessSetting(shouldUpdateScreenBrightnessSetting)
                 .setBrightnessAdjustmentFlag(brightnessAdjustmentFlag)
+                .setIsUserInitiatedChange(true)
                 .build();
 
         assertEquals(displayBrightnessState.getBrightness(), brightness, FLOAT_DELTA);
@@ -109,7 +110,9 @@
                 .append("\n    mBrightnessEvent:")
                 .append(Objects.toString(displayBrightnessState.getBrightnessEvent(), "null"))
                 .append("\n    mBrightnessAdjustmentFlag:")
-                .append(displayBrightnessState.getBrightnessAdjustmentFlag());
+                .append(displayBrightnessState.getBrightnessAdjustmentFlag())
+                .append("\n    mIsUserInitiatedChange:")
+                .append(displayBrightnessState.isUserInitiatedChange());
         return sb.toString();
     }
 }
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 12050e1..01ff35f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -1142,6 +1142,20 @@
     }
 
     @Test
+    public void test_createLocalExternalDisplay_displayManagementEnabled_doesNotCrash()
+            throws Exception {
+        FakeDisplay display = new FakeDisplay(PORT_A);
+        display.info.isInternal = false;
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        when(mSurfaceControlProxy.getDesiredDisplayModeSpecs(display.token)).thenReturn(null);
+        mInjector.getTransmitter().sendHotplug(display, /* connected */ true);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+    }
+
+    @Test
     public void test_createLocalExternalDisplay_displayManagementEnabled_shouldHaveDefaultGroup()
             throws Exception {
         FakeDisplay display = new FakeDisplay(PORT_A);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index c5105e7..323ef6a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -121,7 +121,8 @@
                 any(StrategySelectionRequest.class))).thenReturn(displayBrightnessStrategy);
         mDisplayBrightnessController.updateBrightness(displayPowerRequest, targetDisplayState);
         verify(displayBrightnessStrategy).updateBrightness(
-                eq(new StrategyExecutionRequest(displayPowerRequest, DEFAULT_BRIGHTNESS)));
+                eq(new StrategyExecutionRequest(displayPowerRequest, DEFAULT_BRIGHTNESS,
+                        /* userSetBrightnessChanged= */ false)));
         assertEquals(mDisplayBrightnessController.getCurrentDisplayBrightnessStrategy(),
                 displayBrightnessStrategy);
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategyTest.java
index 7a6a911..4d5ff55 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutoBrightnessFallbackStrategyTest.java
@@ -115,7 +115,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mAutoBrightnessFallbackStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 
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 afb5a5c..8a33f34 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
@@ -513,9 +513,11 @@
                 .setBrightnessEvent(brightnessEvent)
                 .setBrightnessAdjustmentFlag(BrightnessReason.ADJUSTMENT_AUTO)
                 .setShouldUpdateScreenBrightnessSetting(true)
+                .setIsUserInitiatedChange(true)
                 .build();
         DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy
-                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f));
+                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f,
+                        /* userSetBrightnessChanged= */ true));
         assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState);
     }
 
@@ -560,9 +562,11 @@
                 .setBrightnessEvent(brightnessEvent)
                 .setBrightnessAdjustmentFlag(BrightnessReason.ADJUSTMENT_AUTO_TEMP)
                 .setShouldUpdateScreenBrightnessSetting(true)
+                .setIsUserInitiatedChange(true)
                 .build();
         DisplayBrightnessState actualDisplayBrightnessState = mAutomaticBrightnessStrategy
-                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f));
+                .updateBrightness(new StrategyExecutionRequest(displayPowerRequest, 0.6f,
+                        /* userSetBrightnessChanged= */ true));
         assertEquals(expectedDisplayBrightnessState, actualDisplayBrightnessState);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
index 47f1746..3534325 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/BoostBrightnessStrategyTest.java
@@ -60,7 +60,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mBoostBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
index 9246780..bd6d8be 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/DozeBrightnessStrategyTest.java
@@ -57,7 +57,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mDozeBrightnessModeStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FallbackBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FallbackBrightnessStrategyTest.java
index c4767ae..4cae35d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FallbackBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FallbackBrightnessStrategyTest.java
@@ -57,10 +57,12 @@
                         .setSdrBrightness(currentBrightness)
                         .setDisplayBrightnessStrategyName(mFallbackBrightnessStrategy.getName())
                         .setShouldUpdateScreenBrightnessSetting(true)
+                        .setIsUserInitiatedChange(true)
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mFallbackBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, currentBrightness));
+                        new StrategyExecutionRequest(displayPowerRequest, currentBrightness,
+                                /* userSetBrightnessChanged= */ true));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
index 682c9cc..fdaf8f6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/FollowerBrightnessStrategyTest.java
@@ -61,7 +61,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mFollowerBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(expectedDisplayBrightnessState, updatedDisplayBrightnessState);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java
index ccf6309..e3c2760 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OffloadBrightnessStrategyTest.java
@@ -72,7 +72,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mOffloadBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
         assertEquals(PowerManager.BRIGHTNESS_INVALID_FLOAT, mOffloadBrightnessStrategy
                 .getOffloadScreenBrightness(), 0.0f);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
index 8e7b463..ebe407b 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/OverrideBrightnessStrategyTest.java
@@ -60,7 +60,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mOverrideBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
index e799d0e..4bad569 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategyTest.java
@@ -58,7 +58,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mScreenOffBrightnessModeStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
index aaada41..5410a20 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategyTest.java
@@ -60,7 +60,8 @@
                         .build();
         DisplayBrightnessState updatedDisplayBrightnessState =
                 mTemporaryBrightnessStrategy.updateBrightness(
-                        new StrategyExecutionRequest(displayPowerRequest, 0.2f));
+                        new StrategyExecutionRequest(displayPowerRequest, 0.2f,
+                                /* userSetBrightnessChanged= */ false));
         assertEquals(updatedDisplayBrightnessState, expectedDisplayBrightnessState);
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
index cf6146f..34c6ba9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
@@ -117,11 +117,11 @@
             BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), RenderVote(60f, 90f)),
         PREFERRED_REFRESH_RATE(false, 0, 60f, 0f, 0f,
             BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), null),
-        PREFERRED_REFRESH_RATE_IGNORED(true, 0, 60f, 0f, 0f,
-            null, null, null),
+        PREFERRED_REFRESH_RATE_IGNORE_BASE_MODE_CONVERSION(true, 0, 60f, 0f, 0f,
+            RequestedRefreshRateVote(60f), null, null),
         PREFERRED_REFRESH_RATE_INVALID(false, 0, 25f, 0f, 0f,
             null, null, null),
         SYNTHETIC_MODE(false, 99, 0f, 0f, 0f,
-            RenderVote(45f, 45f), SizeVote(1000, 1000, 1000, 1000), null),
+            RequestedRefreshRateVote(45f), SizeVote(1000, 1000, 1000, 1000), null),
     }
 }
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt
new file mode 100644
index 0000000..dbe9e4a
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/RequestedRefreshRateVoteTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RequestedRefreshRateVoteTest {
+
+    @Test
+    fun `updates requestedRefreshRates`() {
+        val refreshRate = 90f
+        val vote = RequestedRefreshRateVote(refreshRate)
+        val summary = createVotesSummary()
+
+        vote.updateSummary(summary)
+
+        assertThat(summary.requestedRefreshRates).hasSize(1)
+        assertThat(summary.requestedRefreshRates).contains(refreshRate)
+    }
+
+    @Test
+    fun `updates requestedRefreshRates with multiple refresh rates`() {
+        val refreshRate1 = 90f
+        val vote1 = RequestedRefreshRateVote(refreshRate1)
+
+        val refreshRate2 = 60f
+        val vote2 = RequestedRefreshRateVote(refreshRate2)
+
+        val summary = createVotesSummary()
+
+        vote1.updateSummary(summary)
+        vote2.updateSummary(summary)
+
+        assertThat(summary.requestedRefreshRates).hasSize(2)
+        assertThat(summary.requestedRefreshRates).containsExactly(refreshRate1, refreshRate2)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
index 5da1bb6..dd5e1be 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
@@ -152,13 +152,51 @@
 
         assertThat(result.map { it.modeId }).containsExactlyElementsIn(testCase.expectedModeIds)
     }
+
+    @Test
+    fun `summary invalid if has requestedRefreshRate less than minRenederRate`() {
+        val summary = createSummary()
+        summary.requestedRefreshRates = setOf(30f, 90f)
+        summary.minRenderFrameRate = 60f
+        summary.maxRenderFrameRate = 120f
+
+        val result = summary.filterModes(arrayOf(createMode(1, 90f, 90f)))
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun `summary invalid if has requestedRefreshRate more than maxRenderFrameRate`() {
+        val summary = createSummary()
+        summary.requestedRefreshRates = setOf(60f, 240f)
+        summary.minRenderFrameRate = 60f
+        summary.maxRenderFrameRate = 120f
+
+        val result = summary.filterModes(arrayOf(createMode(1, 90f, 90f)))
+
+        assertThat(result).isEmpty()
+    }
+
+    @Test
+    fun `summary valid if all requestedRefreshRates inside render rate limits`() {
+        val summary = createSummary()
+        summary.requestedRefreshRates = setOf(60f, 90f)
+        summary.minRenderFrameRate = 60f
+        summary.maxRenderFrameRate = 120f
+
+        val result = summary.filterModes(arrayOf(createMode(1, 90f, 90f)))
+
+        assertThat(result).hasSize(1)
+    }
 }
+
+
 private fun createMode(modeId: Int, refreshRate: Float, vsyncRate: Float): Display.Mode {
     return Display.Mode(modeId, 600, 800, refreshRate, vsyncRate, false,
             FloatArray(0), IntArray(0))
 }
 
-private fun createSummary(supportedModesVoteEnabled: Boolean): VoteSummary {
+private fun createSummary(supportedModesVoteEnabled: Boolean = false): VoteSummary {
     val summary = createVotesSummary(supportedModesVoteEnabled = supportedModesVoteEnabled)
     summary.width = 600
     summary.height = 800
diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
index 0532e04..2a67029 100644
--- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.trust;
 
+import static android.service.trust.TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE;
+
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
@@ -26,12 +28,22 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import static java.util.Collections.singleton;
+
 import android.Manifest;
 import android.annotation.Nullable;
 import android.app.ActivityManager;
+import android.app.AlarmManager;
 import android.app.IActivityManager;
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.ITrustListener;
@@ -65,15 +77,22 @@
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.security.KeyStoreAuthorization;
+import android.service.trust.GrantTrustResult;
+import android.service.trust.ITrustAgentService;
+import android.service.trust.ITrustAgentServiceCallback;
 import android.service.trust.TrustAgentService;
 import android.testing.TestableContext;
+import android.util.Log;
 import android.view.IWindowManager;
 import android.view.WindowManagerGlobal;
 
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.internal.infra.AndroidFuture;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker;
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.StrongAuthFlags;
+import com.android.internal.widget.LockSettingsInternal;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -81,15 +100,19 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class TrustManagerServiceTest {
 
@@ -115,21 +138,28 @@
     private static final int PROFILE_USER_ID = 70;
     private static final long[] PARENT_BIOMETRIC_SIDS = new long[] { 600L, 601L };
     private static final long[] PROFILE_BIOMETRIC_SIDS = new long[] { 700L, 701L };
+    private static final long RENEWABLE_TRUST_DURATION = 10000L;
+    private static final String GRANT_TRUST_MESSAGE = "granted";
+    private static final String TAG = "TrustManagerServiceTest";
 
     private final ArrayList<ResolveInfo> mTrustAgentResolveInfoList = new ArrayList<>();
     private final ArrayList<ComponentName> mKnownTrustAgents = new ArrayList<>();
     private final ArrayList<ComponentName> mEnabledTrustAgents = new ArrayList<>();
+    private final Map<ComponentName, ITrustAgentService.Stub> mMockTrustAgents = new HashMap<>();
 
     private @Mock ActivityManager mActivityManager;
+    private @Mock AlarmManager mAlarmManager;
     private @Mock BiometricManager mBiometricManager;
     private @Mock DevicePolicyManager mDevicePolicyManager;
     private @Mock FaceManager mFaceManager;
     private @Mock FingerprintManager mFingerprintManager;
     private @Mock KeyStoreAuthorization mKeyStoreAuthorization;
     private @Mock LockPatternUtils mLockPatternUtils;
+    private @Mock LockSettingsInternal mLockSettingsInternal;
     private @Mock PackageManager mPackageManager;
     private @Mock UserManager mUserManager;
     private @Mock IWindowManager mWindowManager;
+    private @Mock ITrustListener mTrustListener;
 
     private HandlerThread mHandlerThread;
     private TrustManagerService mService;
@@ -158,6 +188,9 @@
             return null;
         }).when(mLockPatternUtils).setEnabledTrustAgents(any(), eq(TEST_USER_ID));
 
+        LocalServices.removeServiceForTest(LockSettingsInternal.class);
+        LocalServices.addService(LockSettingsInternal.class, mLockSettingsInternal);
+
         ArgumentMatcher<Intent> trustAgentIntentMatcher = new ArgumentMatcher<Intent>() {
             @Override
             public boolean matches(Intent argument) {
@@ -176,6 +209,7 @@
         when(mWindowManager.isKeyguardLocked()).thenReturn(true);
 
         mMockContext.addMockSystemService(ActivityManager.class, mActivityManager);
+        mMockContext.addMockSystemService(AlarmManager.class, mAlarmManager);
         mMockContext.addMockSystemService(BiometricManager.class, mBiometricManager);
         mMockContext.addMockSystemService(FaceManager.class, mFaceManager);
         mMockContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
@@ -197,6 +231,7 @@
         verify(() -> ServiceManager.addService(eq(Context.TRUST_SERVICE),
                     binderArgumentCaptor.capture(), anyBoolean(), anyInt()));
         mTrustManager = ITrustManager.Stub.asInterface(binderArgumentCaptor.getValue());
+        mTrustManager.registerTrustListener(mTrustListener);
     }
 
     private class MockInjector extends TrustManagerService.Injector {
@@ -215,6 +250,11 @@
         }
 
         @Override
+        AlarmManager getAlarmManager() {
+            return mAlarmManager;
+        }
+
+        @Override
         Looper getLooper() {
             return mHandlerThread.getLooper();
         }
@@ -367,12 +407,10 @@
 
     @Test
     public void reportEnabledTrustAgentsChangedInformsListener() throws RemoteException {
-        final ITrustListener trustListener = mock(ITrustListener.class);
-        mTrustManager.registerTrustListener(trustListener);
         mService.waitForIdle();
         mTrustManager.reportEnabledTrustAgentsChanged(TEST_USER_ID);
         mService.waitForIdle();
-        verify(trustListener).onEnabledTrustAgentsChanged(TEST_USER_ID);
+        verify(mTrustListener).onEnabledTrustAgentsChanged(TEST_USER_ID);
     }
 
     // Tests that when the device is locked for a managed profile with a *unified* challenge, the
@@ -416,6 +454,169 @@
     }
 
     @Test
+    public void testSuccessfulUnlock_bindsTrustAgent() throws Exception {
+        when(mUserManager.getAliveUsers())
+                .thenReturn(List.of(new UserInfo(TEST_USER_ID, "test", UserInfo.FLAG_FULL)));
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        ITrustAgentService trustAgentService =
+                setUpTrustAgentWithStrongAuthRequired(
+                        trustAgentName, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+
+        attemptSuccessfulUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        assertThat(getCallback(trustAgentService)).isNotNull();
+    }
+
+    @Test
+    public void testSuccessfulUnlock_notifiesTrustAgent() throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        ITrustAgentService trustAgentService =
+                setUpTrustAgentWithStrongAuthRequired(
+                        trustAgentName, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+
+        attemptSuccessfulUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        verify(trustAgentService).onUnlockAttempt(/* successful= */ true);
+    }
+
+    @Test
+    public void testSuccessfulUnlock_notifiesTrustListenerOfChangeInManagedTrust()
+            throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        setUpTrustAgentWithStrongAuthRequired(trustAgentName, STRONG_AUTH_NOT_REQUIRED);
+        mService.waitForIdle();
+        Mockito.reset(mTrustListener);
+
+        attemptSuccessfulUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        verify(mTrustListener).onTrustManagedChanged(false, TEST_USER_ID);
+    }
+
+    @Test
+    @Ignore("TODO: b/340891566 - Trustagent always refreshes trustable timer for user 0 on unlock")
+    public void testSuccessfulUnlock_refreshesTrustableTimers() throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        ITrustAgentService trustAgent =
+                setUpTrustAgentWithStrongAuthRequired(trustAgentName, STRONG_AUTH_NOT_REQUIRED);
+        setUpRenewableTrust(trustAgent);
+
+        attemptSuccessfulUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        // Idle and hard timeout alarms for first renewable trust granted
+        // Idle timeout alarm refresh for second renewable trust granted
+        // Idle and hard timeout alarms refresh for last report
+        verify(mAlarmManager, times(3))
+                .setExact(
+                        eq(AlarmManager.ELAPSED_REALTIME_WAKEUP),
+                        anyLong(),
+                        anyString(),
+                        any(AlarmManager.OnAlarmListener.class),
+                        any(Handler.class));
+    }
+
+    @Test
+    public void testFailedUnlock_doesNotBindTrustAgent() throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        ITrustAgentService trustAgentService =
+                setUpTrustAgentWithStrongAuthRequired(
+                        trustAgentName, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+
+        attemptFailedUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        verify(trustAgentService, never()).setCallback(any());
+    }
+
+    @Test
+    public void testFailedUnlock_notifiesTrustAgent() throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        ITrustAgentService trustAgentService =
+                setUpTrustAgentWithStrongAuthRequired(trustAgentName, STRONG_AUTH_NOT_REQUIRED);
+
+        attemptFailedUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        verify(trustAgentService).onUnlockAttempt(/* successful= */ false);
+    }
+
+    @Test
+    public void testFailedUnlock_doesNotNotifyTrustListenerOfChangeInManagedTrust()
+            throws Exception {
+        ComponentName trustAgentName =
+                ComponentName.unflattenFromString("com.android/.SystemTrustAgent");
+        setUpTrustAgentWithStrongAuthRequired(trustAgentName, STRONG_AUTH_NOT_REQUIRED);
+        Mockito.reset(mTrustListener);
+
+        attemptFailedUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+
+        verify(mTrustListener, never()).onTrustManagedChanged(anyBoolean(), anyInt());
+    }
+
+    private void setUpRenewableTrust(ITrustAgentService trustAgent) throws RemoteException {
+        ITrustAgentServiceCallback callback = getCallback(trustAgent);
+        callback.setManagingTrust(true);
+        mService.waitForIdle();
+        attemptSuccessfulUnlock(TEST_USER_ID);
+        mService.waitForIdle();
+        when(mWindowManager.isKeyguardLocked()).thenReturn(false);
+        grantRenewableTrust(callback);
+    }
+
+    private ITrustAgentService setUpTrustAgentWithStrongAuthRequired(
+            ComponentName agentName, @StrongAuthFlags int strongAuthFlags) throws Exception {
+        doReturn(true).when(mUserManager).isUserUnlockingOrUnlocked(TEST_USER_ID);
+        addTrustAgent(agentName, true);
+        mLockPatternUtils.setKnownTrustAgents(singleton(agentName), TEST_USER_ID);
+        mLockPatternUtils.setEnabledTrustAgents(singleton(agentName), TEST_USER_ID);
+        when(mUserManager.isUserUnlockingOrUnlocked(TEST_USER_ID)).thenReturn(true);
+        setupStrongAuthTracker(strongAuthFlags, false);
+        mService.waitForIdle();
+        return getOrCreateMockTrustAgent(agentName);
+    }
+
+    private void attemptSuccessfulUnlock(int userId) throws RemoteException {
+        mTrustManager.reportUnlockAttempt(/* successful= */ true, userId);
+    }
+
+    private void attemptFailedUnlock(int userId) throws RemoteException {
+        mTrustManager.reportUnlockAttempt(/* successful= */ false, userId);
+    }
+
+    private void grantRenewableTrust(ITrustAgentServiceCallback callback) throws RemoteException {
+        Log.i(TAG, "Granting trust");
+        AndroidFuture<GrantTrustResult> future = new AndroidFuture<>();
+        callback.grantTrust(
+                GRANT_TRUST_MESSAGE,
+                RENEWABLE_TRUST_DURATION,
+                FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE,
+                future);
+        mService.waitForIdle();
+    }
+
+    /**
+     * Retrieve the ITrustAgentServiceCallback attached to a TrustAgentService after it has been
+     * bound to by the TrustManagerService. Will fail if no binding was established.
+     */
+    private ITrustAgentServiceCallback getCallback(ITrustAgentService trustAgentService)
+            throws RemoteException {
+        ArgumentCaptor<ITrustAgentServiceCallback> callbackCaptor =
+                ArgumentCaptor.forClass(ITrustAgentServiceCallback.class);
+        verify(trustAgentService).setCallback(callbackCaptor.capture());
+        return callbackCaptor.getValue();
+    }
+
+    @Test
     @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS_V2)
     public void testKeystoreWeakUnlockEnabled_whenWeakFingerprintIsSetupAndAllowed()
             throws Exception {
@@ -637,6 +838,20 @@
         ResolveInfo resolveInfo = new ResolveInfo();
         resolveInfo.serviceInfo = serviceInfo;
         mTrustAgentResolveInfoList.add(resolveInfo);
+        ITrustAgentService.Stub mockService = getOrCreateMockTrustAgent(agentComponentName);
+        mMockContext.addMockService(agentComponentName, mockService);
+        mMockTrustAgents.put(agentComponentName, mockService);
+    }
+
+    private ITrustAgentService.Stub getOrCreateMockTrustAgent(ComponentName agentComponentName) {
+        return mMockTrustAgents.computeIfAbsent(
+                agentComponentName,
+                (componentName) -> {
+                    ITrustAgentService.Stub mockTrustAgent = mock(ITrustAgentService.Stub.class);
+                    when(mockTrustAgent.queryLocalInterface(anyString()))
+                            .thenReturn(mockTrustAgent);
+                    return mockTrustAgent;
+                });
     }
 
     private void bootService() {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index c4946f0..8914696 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -432,7 +432,6 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES)
     public void binderDied_resetA11yServiceInfo() {
         final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
         setServiceBinding(COMPONENT_NAME);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
index 52b33db..d4f0d5a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
@@ -593,6 +593,12 @@
         }
 
         @Override
+        public void onMagnificationSystemUIConnectionChanged(boolean connected)
+                throws RemoteException {
+
+        }
+
+        @Override
         public void onMagnificationChanged(int displayId, Region region, MagnificationConfig config)
                 throws RemoteException {
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
index 87fe6cf..0de5807 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationConnectionManagerTest.java
@@ -59,6 +59,7 @@
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.FlakyTest;
 
+import com.android.compatibility.common.util.TestUtils;
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.accessibility.AccessibilityTraceManager;
@@ -76,6 +77,7 @@
  */
 public class MagnificationConnectionManagerTest {
 
+    private static final int WAIT_CONNECTION_TIMEOUT_SECOND = 1;
     private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM;
     private static final int SERVICE_ID = 1;
 
@@ -115,17 +117,19 @@
     private void stubSetConnection(boolean needDelay) {
         doAnswer((InvocationOnMock invocation) -> {
             final boolean connect = (Boolean) invocation.getArguments()[0];
-            // Simulates setConnection() called by another process.
+            // Use post to simulate setConnection() called by another process.
+            final Context context = ApplicationProvider.getApplicationContext();
             if (needDelay) {
-                final Context context = ApplicationProvider.getApplicationContext();
                 context.getMainThreadHandler().postDelayed(
                         () -> {
                             mMagnificationConnectionManager.setConnection(
                                     connect ? mMockConnection.getConnection() : null);
                         }, 10);
             } else {
-                mMagnificationConnectionManager.setConnection(
-                        connect ? mMockConnection.getConnection() : null);
+                context.getMainThreadHandler().post(() -> {
+                    mMagnificationConnectionManager.setConnection(
+                            connect ? mMockConnection.getConnection() : null);
+                });
             }
             return true;
         }).when(mMockStatusBarManagerInternal).requestMagnificationConnection(anyBoolean());
@@ -629,9 +633,10 @@
     }
 
     @Test
-    public void isConnected_requestConnection_expectedValue() throws RemoteException {
+    public void isConnected_requestConnection_expectedValue() throws Exception {
         mMagnificationConnectionManager.requestConnection(true);
-        assertTrue(mMagnificationConnectionManager.isConnected());
+        TestUtils.waitUntil("connection is not ready", WAIT_CONNECTION_TIMEOUT_SECOND,
+                () -> mMagnificationConnectionManager.isConnected());
 
         mMagnificationConnectionManager.requestConnection(false);
         assertFalse(mMagnificationConnectionManager.isConnected());
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index e756082..758c84a 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.media.AudioDeviceAttributes;
@@ -37,6 +38,7 @@
 import android.media.AudioSystem;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.VolumeInfo;
+import android.os.PermissionEnforcer;
 import android.os.RemoteException;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
@@ -98,7 +100,8 @@
 
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
-                mTestLooper.getLooper()) {
+                mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+                mock(AudioServerPermissionProvider.class)) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 3623012..2cb02bd 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -23,12 +23,14 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.media.VolumeInfo;
+import android.os.PermissionEnforcer;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -75,7 +77,8 @@
         mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase();
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
-                mTestLooper.getLooper()) {
+                mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+                mock(AudioServerPermissionProvider.class)) {
             @Override
             public int getDeviceForStream(int stream) {
                 return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
new file mode 100644
index 0000000..8d772ad
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServerPermissionProviderTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.media.permission.INativePermissionController;
+import com.android.media.permission.UidPackageState;
+import com.android.server.pm.pkg.PackageState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public final class AudioServerPermissionProviderTest {
+
+    // Class under test
+    private AudioServerPermissionProvider mPermissionProvider;
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock public INativePermissionController mMockPc;
+
+    @Mock public PackageState mMockPackageStateOne_10000_one;
+    @Mock public PackageState mMockPackageStateTwo_10001_two;
+    @Mock public PackageState mMockPackageStateThree_10000_one;
+    @Mock public PackageState mMockPackageStateFour_10000_three;
+    @Mock public PackageState mMockPackageStateFive_10001_four;
+    @Mock public PackageState mMockPackageStateSix_10000_two;
+
+    public List<UidPackageState> mInitPackageListExpected;
+
+    // Argument matcher which matches that the state is equal even if the package names are out of
+    // order (since they are logically a set).
+    public static final class UidPackageStateMatcher implements ArgumentMatcher<UidPackageState> {
+        private final int mUid;
+        private final List<String> mSortedPackages;
+
+        public UidPackageStateMatcher(int uid, List<String> packageNames) {
+            mUid = uid;
+            if (packageNames != null) {
+                mSortedPackages = new ArrayList(packageNames);
+                Collections.sort(mSortedPackages);
+            } else {
+                mSortedPackages = null;
+            }
+        }
+
+        public UidPackageStateMatcher(UidPackageState toMatch) {
+            this(toMatch.uid, toMatch.packageNames);
+        }
+
+        @Override
+        public boolean matches(UidPackageState state) {
+            if (state == null) return false;
+            if (state.uid != mUid) return false;
+            if ((state.packageNames == null) != (mSortedPackages == null)) return false;
+            var copy = new ArrayList(state.packageNames);
+            Collections.sort(copy);
+            return mSortedPackages.equals(copy);
+        }
+
+        @Override
+        public String toString() {
+            return "Matcher for UidState with uid: " + mUid + ": " + mSortedPackages;
+        }
+    }
+
+    public static final class PackageStateListMatcher
+            implements ArgumentMatcher<List<UidPackageState>> {
+
+        private final List<UidPackageState> mToMatch;
+
+        public PackageStateListMatcher(List<UidPackageState> toMatch) {
+            mToMatch = Objects.requireNonNull(toMatch);
+        }
+
+        @Override
+        public boolean matches(List<UidPackageState> other) {
+            if (other == null) return false;
+            if (other.size() != mToMatch.size()) return false;
+            for (int i = 0; i < mToMatch.size(); i++) {
+                var matcher = new UidPackageStateMatcher(mToMatch.get(i));
+                if (!matcher.matches(other.get(i))) return false;
+            }
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return "Matcher for List<UidState> with uid: " + mToMatch;
+        }
+    }
+
+    @Before
+    public void setup() {
+        when(mMockPackageStateOne_10000_one.getAppId()).thenReturn(10000);
+        when(mMockPackageStateOne_10000_one.getPackageName()).thenReturn("com.package.one");
+
+        when(mMockPackageStateTwo_10001_two.getAppId()).thenReturn(10001);
+        when(mMockPackageStateTwo_10001_two.getPackageName()).thenReturn("com.package.two");
+
+        // Same state as the first is intentional, emulating multi-user
+        when(mMockPackageStateThree_10000_one.getAppId()).thenReturn(10000);
+        when(mMockPackageStateThree_10000_one.getPackageName()).thenReturn("com.package.one");
+
+        when(mMockPackageStateFour_10000_three.getAppId()).thenReturn(10000);
+        when(mMockPackageStateFour_10000_three.getPackageName()).thenReturn("com.package.three");
+
+        when(mMockPackageStateFive_10001_four.getAppId()).thenReturn(10001);
+        when(mMockPackageStateFive_10001_four.getPackageName()).thenReturn("com.package.four");
+
+        when(mMockPackageStateSix_10000_two.getAppId()).thenReturn(10000);
+        when(mMockPackageStateSix_10000_two.getPackageName()).thenReturn("com.package.two");
+    }
+
+    @Test
+    public void testInitialPackagePopulation() throws Exception {
+        var initPackageListData =
+                List.of(
+                        mMockPackageStateOne_10000_one,
+                        mMockPackageStateTwo_10001_two,
+                        mMockPackageStateThree_10000_one,
+                        mMockPackageStateFour_10000_three,
+                        mMockPackageStateFive_10001_four,
+                        mMockPackageStateSix_10000_two);
+        var expectedPackageList =
+                List.of(
+                        createUidPackageState(
+                                10000,
+                                List.of("com.package.one", "com.package.two", "com.package.three")),
+                        createUidPackageState(
+                                10001, List.of("com.package.two", "com.package.four")));
+
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+        verify(mMockPc)
+                .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList)));
+    }
+
+    @Test
+    public void testOnModifyPackageState_whenNewUid() throws Exception {
+        // 10000: one | 10001: two
+        var initPackageListData =
+                List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+
+        // new uid, including user component
+        mPermissionProvider.onModifyPackageState(1_10002, "com.package.new", false /* isRemove */);
+
+        verify(mMockPc)
+                .updatePackagesForUid(
+                        argThat(new UidPackageStateMatcher(10002, List.of("com.package.new"))));
+        verify(mMockPc).updatePackagesForUid(any()); // exactly once
+    }
+
+    @Test
+    public void testOnModifyPackageState_whenRemoveUid() throws Exception {
+        // 10000: one | 10001: two
+        var initPackageListData =
+                List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+
+        // Includes user-id
+        mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+
+        verify(mMockPc).updatePackagesForUid(argThat(new UidPackageStateMatcher(10000, List.of())));
+        verify(mMockPc).updatePackagesForUid(any()); // exactly once
+    }
+
+    @Test
+    public void testOnModifyPackageState_whenUpdatedUidAddition() throws Exception {
+        // 10000: one | 10001: two
+        var initPackageListData =
+                List.of(mMockPackageStateOne_10000_one, mMockPackageStateTwo_10001_two);
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+
+        // Includes user-id
+        mPermissionProvider.onModifyPackageState(1_10000, "com.package.new", false /* isRemove */);
+
+        verify(mMockPc)
+                .updatePackagesForUid(
+                        argThat(
+                                new UidPackageStateMatcher(
+                                        10000, List.of("com.package.one", "com.package.new"))));
+        verify(mMockPc).updatePackagesForUid(any()); // exactly once
+    }
+
+    @Test
+    public void testOnModifyPackageState_whenUpdateUidRemoval() throws Exception {
+        // 10000: one, two | 10001: two
+        var initPackageListData =
+                List.of(
+                        mMockPackageStateOne_10000_one,
+                        mMockPackageStateTwo_10001_two,
+                        mMockPackageStateSix_10000_two);
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+
+        // Includes user-id
+        mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+
+        verify(mMockPc)
+                .updatePackagesForUid(
+                        argThat(
+                                new UidPackageStateMatcher(
+                                        createUidPackageState(10000, List.of("com.package.two")))));
+        verify(mMockPc).updatePackagesForUid(any()); // exactly once
+    }
+
+    @Test
+    public void testOnServiceStart() throws Exception {
+        // 10000: one, two | 10001: two
+        var initPackageListData =
+                List.of(
+                        mMockPackageStateOne_10000_one,
+                        mMockPackageStateTwo_10001_two,
+                        mMockPackageStateSix_10000_two);
+        mPermissionProvider = new AudioServerPermissionProvider(initPackageListData);
+        mPermissionProvider.onServiceStart(mMockPc);
+        mPermissionProvider.onModifyPackageState(1_10000, "com.package.one", true /* isRemove */);
+        verify(mMockPc)
+                .updatePackagesForUid(
+                        argThat(new UidPackageStateMatcher(10000, List.of("com.package.two"))));
+
+        verify(mMockPc).updatePackagesForUid(any()); // exactly once
+        mPermissionProvider.onModifyPackageState(
+                1_10000, "com.package.three", false /* isRemove */);
+        verify(mMockPc)
+                .updatePackagesForUid(
+                        argThat(
+                                new UidPackageStateMatcher(
+                                        10000, List.of("com.package.two", "com.package.three"))));
+        verify(mMockPc, times(2)).updatePackagesForUid(any()); // exactly twice
+        // state is now 10000: two, three | 10001: two
+
+        // simulate restart of the service
+        mPermissionProvider.onServiceStart(null); // should handle null
+        var newMockPc = mock(INativePermissionController.class);
+        mPermissionProvider.onServiceStart(newMockPc);
+
+        var expectedPackageList =
+                List.of(
+                        createUidPackageState(
+                                10000, List.of("com.package.two", "com.package.three")),
+                        createUidPackageState(10001, List.of("com.package.two")));
+
+        verify(newMockPc)
+                .populatePackagesForUids(argThat(new PackageStateListMatcher(expectedPackageList)));
+
+        verify(newMockPc, never()).updatePackagesForUid(any());
+        // updates should still work after restart
+        mPermissionProvider.onModifyPackageState(10001, "com.package.four", false /* isRemove */);
+        verify(newMockPc)
+                .updatePackagesForUid(
+                        argThat(
+                                new UidPackageStateMatcher(
+                                        10001, List.of("com.package.two", "com.package.four"))));
+        // exactly once
+        verify(newMockPc).updatePackagesForUid(any());
+    }
+
+    private static UidPackageState createUidPackageState(int uid, List<String> packages) {
+        var res = new UidPackageState();
+        res.uid = uid;
+        res.packageNames = packages;
+        return res;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index 634877e..037c3c0 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -66,6 +66,7 @@
     @Mock private AppOpsManager mMockAppOpsManager;
     @Mock private AudioPolicyFacade mMockAudioPolicy;
     @Mock private PermissionEnforcer mMockPermissionEnforcer;
+    @Mock private AudioServerPermissionProvider mMockPermissionProvider;
 
     // the class being unit-tested here
     private AudioService mAudioService;
@@ -86,7 +87,7 @@
                 .thenReturn(AppOpsManager.MODE_ALLOWED);
         mAudioService = new AudioService(mContext, mSpyAudioSystem, mSpySystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy, null,
-                mMockAppOpsManager, mMockPermissionEnforcer);
+                mMockAppOpsManager, mMockPermissionEnforcer, mMockPermissionProvider);
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index 8dfcc18..27b552f 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -22,11 +22,13 @@
 import static org.mockito.Mockito.mock;
 
 import android.annotation.NonNull;
+import android.app.AppOpsManager;
 import android.content.Context;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IDeviceVolumeBehaviorDispatcher;
+import android.os.PermissionEnforcer;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 
@@ -75,7 +77,8 @@
         mAudioVolumeGroupHelper = new AudioVolumeGroupHelperBase();
         mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
                 mSettingsAdapter, mAudioVolumeGroupHelper, mAudioPolicyMock,
-                mTestLooper.getLooper());
+                mTestLooper.getLooper(), mock(AppOpsManager.class), mock(PermissionEnforcer.class),
+                mock(AudioServerPermissionProvider.class));
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 23728db..8e34ee1 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -40,6 +40,7 @@
 import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
 
 import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -132,9 +133,12 @@
     @Mock
     private PermissionEnforcer mMockPermissionEnforcer;
     @Mock
+    private AudioServerPermissionProvider mMockPermissionProvider;
+    @Mock
     private AudioVolumeGroupHelperBase mAudioVolumeGroupHelper;
 
-    private final AudioPolicyFacade mFakeAudioPolicy = lookbackAudio -> false;
+    @Mock
+    private AudioPolicyFacade mMockAudioPolicy;
 
     private AudioVolumeGroup mAudioMusicVolumeGroup;
 
@@ -153,9 +157,10 @@
                 SystemServerAdapter systemServer, SettingsAdapter settings,
                 AudioVolumeGroupHelperBase audioVolumeGroupHelper, AudioPolicyFacade audioPolicy,
                 @Nullable Looper looper, AppOpsManager appOps,
-                @NonNull PermissionEnforcer enforcer) {
+                @NonNull PermissionEnforcer enforcer,
+                AudioServerPermissionProvider permissionProvider) {
             super(context, audioSystem, systemServer, settings, audioVolumeGroupHelper,
-                    audioPolicy, looper, appOps, enforcer);
+                    audioPolicy, looper, appOps, enforcer, permissionProvider);
         }
 
         public void setDeviceForStream(int stream, int device) {
@@ -209,8 +214,9 @@
         mAm = mContext.getSystemService(AudioManager.class);
 
         mAudioService = new MyAudioService(mContext, mSpyAudioSystem, mSpySystemServer,
-                mSettingsAdapter, mAudioVolumeGroupHelper, mFakeAudioPolicy,
-                mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer);
+                mSettingsAdapter, mAudioVolumeGroupHelper, mMockAudioPolicy,
+                mTestLooper.getLooper(), mMockAppOpsManager, mMockPermissionEnforcer,
+                mMockPermissionProvider);
 
         mTestLooper.dispatchAll();
         prepareAudioServiceState();
@@ -552,7 +558,7 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+    @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX})
     public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception {
         final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
         final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
@@ -607,6 +613,7 @@
 
     @Test
     @RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+    @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX)
     public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception {
         final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
         final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 7dd1847..50cfa75 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -20,7 +20,6 @@
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
-import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -52,6 +51,7 @@
 import android.util.SparseArray;
 import android.util.Xml;
 
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.internal.content.PackageMonitor;
@@ -70,6 +70,7 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
@@ -95,21 +96,21 @@
     private static final int DEFAULT_USER_ID = 0;
     private static final int WORK_PROFILE_USER_ID = 10;
     private static final int DEFAULT_UID = Binder.getCallingUid() + 100;
+    private static final int WORK_PROFILE_UID = Binder.getCallingUid() + 1000100;
     private static final long DEFAULT_CREATION_TIME_MILLIS = 1000;
     private static final Duration RETENTION_PERIOD = Duration.ofDays(3);
     private static final LocaleList DEFAULT_LOCALES =
             LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS);
     private static final Map<String, LocalesInfo> DEFAULT_PACKAGE_LOCALES_INFO_MAP = Map.of(
             DEFAULT_PACKAGE_NAME, new LocalesInfo(DEFAULT_LOCALE_TAGS, false));
-    private static final SparseArray<LocaleManagerBackupHelper.StagedData> STAGE_DATA =
-            new SparseArray<>();
+    private final SparseArray<File> mStagedDataFiles = new SparseArray<>();
+    private File mArchivedPackageFile;
 
     private LocaleManagerBackupHelper mBackupHelper;
     private long mCurrentTimeMillis;
+    private Context mContext = spy(ApplicationProvider.getApplicationContext());
 
     @Mock
-    private Context mMockContext;
-    @Mock
     private PackageManager mMockPackageManager;
     @Mock
     private LocaleManagerService mMockLocaleManagerService;
@@ -138,23 +139,28 @@
 
     @Before
     public void setUp() throws Exception {
-        mMockContext = mock(Context.class);
         mMockPackageManager = mock(PackageManager.class);
         mMockLocaleManagerService = mock(LocaleManagerService.class);
         mMockDelegateAppLocalePackages = mock(SharedPreferences.class);
         mMockSpEditor = mock(SharedPreferences.Editor.class);
         SystemAppUpdateTracker systemAppUpdateTracker = mock(SystemAppUpdateTracker.class);
 
-        doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
+        doReturn(mMockPackageManager).when(mContext).getPackageManager();
         doReturn(mMockSpEditor).when(mMockDelegateAppLocalePackages).edit();
 
         HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
                 Process.THREAD_PRIORITY_BACKGROUND);
         broadcastHandlerThread.start();
 
-        mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mMockContext,
-                mMockLocaleManagerService, mMockPackageManager, mClock, STAGE_DATA,
-                broadcastHandlerThread, mMockDelegateAppLocalePackages));
+        File file0 = new File(mContext.getCacheDir(), "file_user_0.txt");
+        File file10 = new File(mContext.getCacheDir(), "file_user_10.txt");
+        mStagedDataFiles.put(DEFAULT_USER_ID, file0);
+        mStagedDataFiles.put(WORK_PROFILE_USER_ID, file10);
+        mArchivedPackageFile = new File(mContext.getCacheDir(), "file_archived.txt");
+
+        mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mContext,
+            mMockLocaleManagerService, mMockPackageManager, mClock, broadcastHandlerThread,
+                mStagedDataFiles, mArchivedPackageFile, mMockDelegateAppLocalePackages));
         doNothing().when(mBackupHelper).notifyBackupManager();
 
         mUserMonitor = mBackupHelper.getUserMonitor();
@@ -165,7 +171,16 @@
 
     @After
     public void tearDown() throws Exception {
-        STAGE_DATA.clear();
+        for (int i = 0; i < mStagedDataFiles.size(); i++) {
+            int userId = mStagedDataFiles.keyAt(i);
+            File file = mStagedDataFiles.get(userId);
+            SharedPreferences sp = mContext.getSharedPreferences(file, Context.MODE_PRIVATE);
+            sp.edit().clear().commit();
+            if (file.exists()) {
+                file.delete();
+            }
+        }
+        mStagedDataFiles.clear();
     }
 
     @Test
@@ -543,17 +558,21 @@
         mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, bundle);
         mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, bundle);
 
+        checkArchivedFileExists();
+
         mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
 
         verifyNothingRestored();
 
         setUpPackageInstalled(pkgNameA);
 
-        mPackageMonitor.onPackageUpdateFinished(pkgNameA, DEFAULT_UID);
+        mBackupHelper.onPackageUpdateFinished(pkgNameA, DEFAULT_UID);
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
                 LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+        checkArchivedFileExists();
+
 
         mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false);
 
@@ -565,11 +584,12 @@
 
         setUpPackageInstalled(pkgNameB);
 
-        mPackageMonitor.onPackageUpdateFinished(pkgNameB, DEFAULT_UID);
+        mBackupHelper.onPackageUpdateFinished(pkgNameB, DEFAULT_UID);
 
         verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
                 LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
                 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+        checkArchivedFileDoesNotExist();
 
         mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false);
 
@@ -723,7 +743,7 @@
         Intent intent = new Intent();
         intent.setAction(Intent.ACTION_USER_REMOVED);
         intent.putExtra(Intent.EXTRA_USER_HANDLE, DEFAULT_USER_ID);
-        mUserMonitor.onReceive(mMockContext, intent);
+        mUserMonitor.onReceive(mContext, intent);
 
         // Stage data should be removed only for DEFAULT_USER_ID.
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
@@ -732,6 +752,72 @@
     }
 
     @Test
+    public void testRestore_multipleProfile_restoresFromStage_ArchiveEnabled() throws Exception {
+        final ByteArrayOutputStream outDefault = new ByteArrayOutputStream();
+        writeTestPayload(outDefault, DEFAULT_PACKAGE_LOCALES_INFO_MAP);
+        final ByteArrayOutputStream outWorkProfile = new ByteArrayOutputStream();
+        String anotherPackage = "com.android.anotherapp";
+        String anotherLangTags = "mr,zh";
+        LocalesInfo localesInfo = new LocalesInfo(anotherLangTags, true);
+        HashMap<String, LocalesInfo> pkgLocalesMapWorkProfile = new HashMap<>();
+        pkgLocalesMapWorkProfile.put(anotherPackage, localesInfo);
+        writeTestPayload(outWorkProfile, pkgLocalesMapWorkProfile);
+        // DEFAULT_PACKAGE_NAME is NOT installed on the device.
+        setUpPackageNotInstalled(DEFAULT_PACKAGE_NAME);
+        setUpPackageNotInstalled(anotherPackage);
+        setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.getEmptyLocaleList());
+        setUpLocalesForPackage(anotherPackage, LocaleList.getEmptyLocaleList());
+        setUpPackageNamesForSp(new ArraySet<>());
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+        mPackageMonitor.onPackageAddedWithExtras(DEFAULT_PACKAGE_NAME, DEFAULT_UID, bundle);
+        mPackageMonitor.onPackageAddedWithExtras(anotherPackage, WORK_PROFILE_UID, bundle);
+
+        checkArchivedFileExists();
+
+        mBackupHelper.stageAndApplyRestoredPayload(outDefault.toByteArray(), DEFAULT_USER_ID);
+        mBackupHelper.stageAndApplyRestoredPayload(outWorkProfile.toByteArray(),
+                WORK_PROFILE_USER_ID);
+
+        verifyNothingRestored();
+        verifyStageDataForUser(DEFAULT_PACKAGE_LOCALES_INFO_MAP,
+                DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+        verifyStageDataForUser(pkgLocalesMapWorkProfile,
+                DEFAULT_CREATION_TIME_MILLIS, WORK_PROFILE_USER_ID);
+
+        setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
+        mBackupHelper.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(DEFAULT_PACKAGE_NAME,
+                DEFAULT_USER_ID,
+                LocaleList.forLanguageTags(DEFAULT_LOCALE_TAGS), false, FrameworkStatsLog
+                        .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+        checkArchivedFileExists();
+        checkStageDataDoesNotExist(DEFAULT_USER_ID);
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, DEFAULT_PACKAGE_NAME, false,
+                false);
+
+        verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+
+        setUpPackageInstalled(anotherPackage);
+        mBackupHelper.onPackageUpdateFinished(anotherPackage, WORK_PROFILE_UID);
+
+        verify(mMockLocaleManagerService, times(1)).setApplicationLocales(anotherPackage,
+                WORK_PROFILE_USER_ID,
+                LocaleList.forLanguageTags(anotherLangTags), true, FrameworkStatsLog
+                        .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+        checkArchivedFileDoesNotExist();
+
+        mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, anotherPackage, true, false);
+
+        verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+            new ArraySet<>(Arrays.asList(anotherPackage)));
+        checkStageDataDoesNotExist(WORK_PROFILE_USER_ID);
+    }
+
+    @Test
     public void testPackageRemoved_noInfoInSp() throws Exception {
         String pkgNameA = "com.android.myAppA";
         String pkgNameB = "com.android.myAppB";
@@ -858,10 +944,22 @@
 
     private void verifyStageDataForUser(Map<String, LocalesInfo> expectedPkgLocalesMap,
             long expectedCreationTimeMillis, int userId) {
-        LocaleManagerBackupHelper.StagedData stagedDataForUser = STAGE_DATA.get(userId);
-        assertNotNull(stagedDataForUser);
-        assertEquals(expectedCreationTimeMillis, stagedDataForUser.mCreationTimeMillis);
-        verifyStageData(expectedPkgLocalesMap, stagedDataForUser.mPackageStates);
+        SharedPreferences sp = mContext.getSharedPreferences(mStagedDataFiles.get(userId),
+                Context.MODE_PRIVATE);
+        assertTrue(sp.getAll().size() > 0);
+        assertEquals(expectedCreationTimeMillis, sp.getLong("staged_data_time", -1));
+        verifyStageData(expectedPkgLocalesMap, sp);
+    }
+
+    private static void verifyStageData(Map<String, LocalesInfo> expectedPkgLocalesMap,
+            SharedPreferences sp) {
+        for (String pkg : expectedPkgLocalesMap.keySet()) {
+            assertTrue(!sp.getString(pkg, "").isEmpty());
+            String[] info = sp.getString(pkg, "").split(" s:");
+            assertEquals(expectedPkgLocalesMap.get(pkg).mLocales, info[0]);
+            assertEquals(expectedPkgLocalesMap.get(pkg).mSetFromDelegate,
+                    Boolean.parseBoolean(info[1]));
+        }
     }
 
     private static void verifyStageData(Map<String, LocalesInfo> expectedPkgLocalesMap,
@@ -875,11 +973,19 @@
         }
     }
 
-    private static void checkStageDataExists(int userId) {
-        assertNotNull(STAGE_DATA.get(userId));
+    private void checkStageDataExists(int userId) {
+        assertTrue(mStagedDataFiles.get(userId) != null && mStagedDataFiles.get(userId).exists());
     }
 
-    private static void checkStageDataDoesNotExist(int userId) {
-        assertNull(STAGE_DATA.get(userId));
+    private void checkStageDataDoesNotExist(int userId) {
+        assertTrue(mStagedDataFiles.get(userId) == null || !mStagedDataFiles.get(userId).exists());
     }
-}
+
+    private void checkArchivedFileExists() {
+        assertTrue(mArchivedPackageFile.exists());
+    }
+
+    private void checkArchivedFileDoesNotExist() {
+        assertTrue(!mArchivedPackageFile.exists());
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
index 9f7cbe3..b46902d 100644
--- a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
+++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
@@ -22,6 +22,7 @@
 import android.os.HandlerThread;
 import android.util.SparseArray;
 
+import java.io.File;
 import java.time.Clock;
 
 /**
@@ -33,9 +34,9 @@
     ShadowLocaleManagerBackupHelper(Context context,
             LocaleManagerService localeManagerService,
             PackageManager packageManager, Clock clock,
-            SparseArray<LocaleManagerBackupHelper.StagedData> stagedData,
-            HandlerThread broadcastHandlerThread, SharedPreferences delegateAppLocalePackages) {
-        super(context, localeManagerService, packageManager, clock, stagedData,
-                broadcastHandlerThread, delegateAppLocalePackages);
+            HandlerThread broadcastHandlerThread, SparseArray<File> stagedDataFiles,
+            File archivedPackagesFile, SharedPreferences delegateAppLocalePackages) {
+        super(context, localeManagerService, packageManager, clock, broadcastHandlerThread,
+                stagedDataFiles, archivedPackagesFile, delegateAppLocalePackages);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
index 64e6236..17b499e 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -19,6 +19,7 @@
 import static android.content.pm.UserInfo.FLAG_FULL;
 import static android.content.pm.UserInfo.FLAG_PRIMARY;
 import static android.content.pm.UserInfo.FLAG_PROFILE;
+import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID;
 import static android.os.UserHandle.USER_SYSTEM;
 
 import static com.android.internal.widget.LockSettingsInternal.ARM_REBOOT_ERROR_ESCROW_NOT_READY;
@@ -32,6 +33,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyByte;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -65,6 +67,7 @@
 
 import com.android.internal.widget.RebootEscrowListener;
 import com.android.server.locksettings.ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection;
+import com.android.server.pm.UserManagerInternal;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -107,6 +110,7 @@
 
     private Context mContext;
     private UserManager mUserManager;
+    private UserManagerInternal mUserManagerInternal;
     private RebootEscrowManager.Callbacks mCallbacks;
     private IRebootEscrow mRebootEscrow;
     private ResumeOnRebootServiceConnection mServiceConnection;
@@ -126,13 +130,15 @@
         long getCurrentTimeMillis();
 
         void reportMetric(boolean success, int errorCode, int serviceType, int attemptCount,
-                int escrowDurationInSeconds, int vbmetaDigestStatus, int durationSinceBootComplete);
+                          int escrowDurationInSeconds, int vbmetaDigestStatus,
+                          int durationSinceBootComplete);
     }
 
     static class MockInjector extends RebootEscrowManager.Injector {
         private final IRebootEscrow mRebootEscrow;
         private final RebootEscrowProviderInterface mDefaultRebootEscrowProvider;
         private final UserManager mUserManager;
+        private final UserManagerInternal mUserManagerInternal;
         private final MockableRebootEscrowInjected mInjected;
         private final RebootEscrowKeyStoreManager mKeyStoreManager;
         private boolean mServerBased;
@@ -141,12 +147,16 @@
         private Consumer<ConnectivityManager.NetworkCallback> mNetworkConsumer;
         private boolean mWaitForInternet;
 
-        MockInjector(Context context, UserManager userManager,
+        MockInjector(
+                Context context,
+                UserManager userManager,
+                UserManagerInternal userManagerInternal,
                 IRebootEscrow rebootEscrow,
                 RebootEscrowKeyStoreManager keyStoreManager,
                 LockSettingsStorageTestable storage,
                 MockableRebootEscrowInjected injected) {
-            super(context, storage);
+            // TODO: change this
+            super(context, storage, userManagerInternal);
             mRebootEscrow = rebootEscrow;
             mServerBased = false;
             mWaitForInternet = false;
@@ -159,16 +169,20 @@
                     };
             mDefaultRebootEscrowProvider = new RebootEscrowProviderHalImpl(halInjector);
             mUserManager = userManager;
+            mUserManagerInternal = userManagerInternal;
             mKeyStoreManager = keyStoreManager;
             mInjected = injected;
         }
 
-        MockInjector(Context context, UserManager userManager,
+        MockInjector(
+                Context context,
+                UserManager userManager,
+                UserManagerInternal userManagerInternal,
                 ResumeOnRebootServiceConnection serviceConnection,
                 RebootEscrowKeyStoreManager keyStoreManager,
                 LockSettingsStorageTestable storage,
                 MockableRebootEscrowInjected injected) {
-            super(context, storage);
+            super(context, storage, userManagerInternal);
             mRebootEscrow = null;
             mServerBased = true;
             mWaitForInternet = false;
@@ -187,6 +201,7 @@
             mDefaultRebootEscrowProvider = new RebootEscrowProviderServerBasedImpl(
                     storage, injector);
             mUserManager = userManager;
+            mUserManagerInternal = userManagerInternal;
             mKeyStoreManager = keyStoreManager;
             mInjected = injected;
         }
@@ -202,6 +217,11 @@
         }
 
         @Override
+        public UserManagerInternal getUserManagerInternal() {
+            return mUserManagerInternal;
+        }
+
+        @Override
         public boolean serverBasedResumeOnReboot() {
             return mServerBased;
         }
@@ -289,8 +309,8 @@
 
         @Override
         public void reportMetric(boolean success, int errorCode, int serviceType, int attemptCount,
-                int escrowDurationInSeconds, int vbmetaDigestStatus,
-                int durationSinceBootComplete) {
+                                 int escrowDurationInSeconds, int vbmetaDigestStatus,
+                                 int durationSinceBootComplete) {
 
             mInjected.reportMetric(success, errorCode, serviceType, attemptCount,
                     escrowDurationInSeconds, vbmetaDigestStatus, durationSinceBootComplete);
@@ -301,6 +321,7 @@
     public void setUp_baseServices() throws Exception {
         mContext = new ContextWrapper(InstrumentationRegistry.getContext());
         mUserManager = mock(UserManager.class);
+        mUserManagerInternal = mock(UserManagerInternal.class);
         mCallbacks = mock(RebootEscrowManager.Callbacks.class);
         mRebootEscrow = mock(IRebootEscrow.class);
         mServiceConnection = mock(ResumeOnRebootServiceConnection.class);
@@ -314,28 +335,43 @@
                 new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings"));
 
         ArrayList<UserInfo> users = new ArrayList<>();
-        users.add(new UserInfo(PRIMARY_USER_ID, "primary", FLAG_PRIMARY));
-        users.add(new UserInfo(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE));
-        users.add(new UserInfo(NONSECURE_SECONDARY_USER_ID, "non-secure", FLAG_FULL));
-        users.add(new UserInfo(SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL));
+        users.add(createUser(PRIMARY_USER_ID, "primary", FLAG_PRIMARY, PRIMARY_USER_ID));
+        users.add(createUser(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE, PRIMARY_USER_ID));
+        users.add(
+                createUser(
+                        NONSECURE_SECONDARY_USER_ID, "non-secure", FLAG_FULL, NO_PROFILE_GROUP_ID));
+        users.add(createUser(SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL, NO_PROFILE_GROUP_ID));
         when(mUserManager.getUsers()).thenReturn(users);
         when(mCallbacks.isUserSecure(PRIMARY_USER_ID)).thenReturn(true);
         when(mCallbacks.isUserSecure(WORK_PROFILE_USER_ID)).thenReturn(true);
         when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false);
         when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
         mInjected = mock(MockableRebootEscrowInjected.class);
-        mMockInjector = new MockInjector(mContext, mUserManager, mRebootEscrow,
-                mKeyStoreManager, mStorage, mInjected);
+        mMockInjector =
+                new MockInjector(
+                        mContext,
+                        mUserManager,
+                        mUserManagerInternal,
+                        mRebootEscrow,
+                        mKeyStoreManager,
+                        mStorage,
+                        mInjected);
         HandlerThread thread = new HandlerThread("RebootEscrowManagerTest");
         thread.start();
         mHandler = new Handler(thread.getLooper());
         mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
-
     }
 
     private void setServerBasedRebootEscrowProvider() throws Exception {
-        mMockInjector = new MockInjector(mContext, mUserManager, mServiceConnection,
-                mKeyStoreManager, mStorage, mInjected);
+        mMockInjector =
+                new MockInjector(
+                        mContext,
+                        mUserManager,
+                        mUserManagerInternal,
+                        mServiceConnection,
+                        mKeyStoreManager,
+                        mStorage,
+                        mInjected);
         mService = new RebootEscrowManager(mMockInjector, mCallbacks, mStorage, mHandler);
     }
 
@@ -352,6 +388,12 @@
         waitForHandler();
     }
 
+    private UserInfo createUser(int id, String name, int flag, int profileGroupId) {
+        UserInfo user = new UserInfo(id, name, flag);
+        when(mUserManagerInternal.getProfileParentId(eq(id))).thenReturn(profileGroupId);
+        return user;
+    }
+
     @Test
     public void prepareRebootEscrow_Success() throws Exception {
         RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
@@ -559,6 +601,172 @@
     }
 
     @Test
+    public void loadRebootEscrowDataIfAvailable_noDataPrimaryUser_Failure() throws Exception {
+        setServerBasedRebootEscrowProvider();
+        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
+        mService.setRebootEscrowListener(mockListener);
+        mService.prepareRebootEscrow();
+
+        clearInvocations(mServiceConnection);
+
+        // escrow secondary user, don't escrow primary user
+        callToRebootEscrowIfNeededAndWait(SECURE_SECONDARY_USER_ID);
+        verify(mockListener).onPreparedForReboot(eq(true));
+        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
+
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
+        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
+
+        assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID));
+        assertFalse(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+        // pretend reboot happens here
+        when(mInjected.getBootCount()).thenReturn(1);
+        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
+        ArgumentCaptor<Integer> metricsErrorCodeCaptor = ArgumentCaptor.forClass(Integer.class);
+        doNothing()
+                .when(mInjected)
+                .reportMetric(
+                        metricsSuccessCaptor.capture(),
+                        metricsErrorCodeCaptor.capture(),
+                        eq(2) /* Server based */,
+                        eq(1) /* attempt count */,
+                        anyInt(),
+                        eq(0) /* vbmeta status */,
+                        anyInt());
+        mService.loadRebootEscrowDataIfAvailable(null);
+        verify(mServiceConnection, never()).unwrap(any(), anyLong());
+        verify(mCallbacks, never()).onRebootEscrowRestored(anyByte(), any(), anyInt());
+        assertFalse(metricsSuccessCaptor.getValue());
+        assertEquals(
+                Integer.valueOf(RebootEscrowManager.ERROR_NO_REBOOT_ESCROW_DATA),
+                metricsErrorCodeCaptor.getValue());
+    }
+
+    @Test
+    public void loadRebootEscrowDataIfAvailable_noDataSecondaryUser_Success() throws Exception {
+        setServerBasedRebootEscrowProvider();
+        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
+        mService.setRebootEscrowListener(mockListener);
+        mService.prepareRebootEscrow();
+
+        clearInvocations(mServiceConnection);
+
+        // Setup work profile with secondary user as parent.
+        ArrayList<UserInfo> users = new ArrayList<>();
+        users.add(createUser(PRIMARY_USER_ID, "primary", FLAG_PRIMARY, NO_PROFILE_GROUP_ID));
+        users.add(createUser(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE, SECURE_SECONDARY_USER_ID));
+        users.add(
+                createUser(
+                        SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL, SECURE_SECONDARY_USER_ID));
+        when(mUserManager.getUsers()).thenReturn(users);
+
+        // escrow primary user and work profile, don't escrow secondary user
+        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
+        verify(mockListener).onPreparedForReboot(eq(true));
+        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
+        callToRebootEscrowIfNeededAndWait(WORK_PROFILE_USER_ID);
+        verify(mockListener).onPreparedForReboot(eq(true));
+        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
+
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
+        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
+
+        assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
+        assertFalse(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID));
+        assertTrue(mStorage.hasRebootEscrow(WORK_PROFILE_USER_ID));
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+        // pretend reboot happens here
+        when(mInjected.getBootCount()).thenReturn(1);
+        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
+        doNothing()
+                .when(mInjected)
+                .reportMetric(
+                        metricsSuccessCaptor.capture(),
+                        eq(0) /* error code */,
+                        eq(2) /* Server based */,
+                        eq(1) /* attempt count */,
+                        anyInt(),
+                        eq(0) /* vbmeta status */,
+                        anyInt());
+        when(mServiceConnection.unwrap(any(), anyLong()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+
+        mService.loadRebootEscrowDataIfAvailable(null);
+
+        verify(mServiceConnection).unwrap(any(), anyLong());
+        verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
+        verify(mCallbacks, never())
+                .onRebootEscrowRestored(anyByte(), any(), eq(SECURE_SECONDARY_USER_ID));
+        verify(mCallbacks, never())
+                .onRebootEscrowRestored(anyByte(), any(), eq(WORK_PROFILE_USER_ID));
+        verify(mCallbacks, never())
+                .onRebootEscrowRestored(anyByte(), any(), eq(NONSECURE_SECONDARY_USER_ID));
+        assertTrue(metricsSuccessCaptor.getValue());
+    }
+
+    @Test
+    public void loadRebootEscrowDataIfAvailable_noDataWorkProfile_Success() throws Exception {
+        setServerBasedRebootEscrowProvider();
+        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
+        mService.setRebootEscrowListener(mockListener);
+        mService.prepareRebootEscrow();
+
+        clearInvocations(mServiceConnection);
+
+        // escrow primary user and secondary user, don't escrow work profile
+        callToRebootEscrowIfNeededAndWait(PRIMARY_USER_ID);
+        verify(mockListener).onPreparedForReboot(eq(true));
+        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
+        callToRebootEscrowIfNeededAndWait(SECURE_SECONDARY_USER_ID);
+        verify(mockListener).onPreparedForReboot(eq(true));
+        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
+
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+        assertEquals(ARM_REBOOT_ERROR_NONE, mService.armRebootEscrowIfNeeded());
+        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
+
+        assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
+        assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID));
+        assertFalse(mStorage.hasRebootEscrow(WORK_PROFILE_USER_ID));
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+        // pretend reboot happens here
+        when(mInjected.getBootCount()).thenReturn(1);
+        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
+        doNothing()
+                .when(mInjected)
+                .reportMetric(
+                        metricsSuccessCaptor.capture(),
+                        eq(0) /* error code */,
+                        eq(2) /* Server based */,
+                        eq(1) /* attempt count */,
+                        anyInt(),
+                        eq(0) /* vbmeta status */,
+                        anyInt());
+        when(mServiceConnection.unwrap(any(), anyLong()))
+                .thenAnswer(invocation -> invocation.getArgument(0));
+
+        mService.loadRebootEscrowDataIfAvailable(null);
+
+        verify(mServiceConnection).unwrap(any(), anyLong());
+        verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(PRIMARY_USER_ID));
+        verify(mCallbacks).onRebootEscrowRestored(anyByte(), any(), eq(SECURE_SECONDARY_USER_ID));
+        verify(mCallbacks, never())
+                .onRebootEscrowRestored(anyByte(), any(), eq(WORK_PROFILE_USER_ID));
+        verify(mCallbacks, never())
+                .onRebootEscrowRestored(anyByte(), any(), eq(NONSECURE_SECONDARY_USER_ID));
+        assertTrue(metricsSuccessCaptor.getValue());
+    }
+
+    @Test
     public void loadRebootEscrowDataIfAvailable_ServerBased_Success() throws Exception {
         setServerBasedRebootEscrowProvider();
 
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index c7c97e4..8a7d276 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -57,12 +57,12 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
 import com.android.server.UiServiceTestCase;
@@ -79,9 +79,12 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
 @SmallTest
 @SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the class.
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
 public class GroupHelperTest extends UiServiceTestCase {
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
@@ -95,6 +98,16 @@
     private GroupHelper mGroupHelper;
     private @Mock Icon mSmallIcon;
 
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return FlagsParameterization.allCombinationsOf(
+                android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST);
+    }
+
+    public GroupHelperTest(FlagsParameterization flags) {
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -708,7 +721,8 @@
     }
 
     @Test
-    public void testDropToZeroRemoveGroup() {
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testDropToZeroRemoveGroup_disableFlag() {
         final String pkg = "package";
         List<StatusBarNotification> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -736,7 +750,37 @@
     }
 
     @Test
-    public void testAppStartsGrouping() {
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testDropToZeroRemoveGroup() {
+        final String pkg = "package";
+        List<StatusBarNotification> posted = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            posted.add(sbn);
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        Mockito.reset(mCallback);
+
+        for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+            mGroupHelper.onNotificationRemoved(posted.remove(0));
+        }
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        Mockito.reset(mCallback);
+
+        mGroupHelper.onNotificationRemoved(posted.remove(0));
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAppStartsGrouping_disableFlag() {
         final String pkg = "package";
         List<StatusBarNotification> posted = new ArrayList<>();
         for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
@@ -765,6 +809,36 @@
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
+    public void testAppStartsGrouping() {
+        final String pkg = "package";
+        List<StatusBarNotification> posted = new ArrayList<>();
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            final StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM);
+            posted.add(sbn);
+            mGroupHelper.onNotificationPosted(sbn, false);
+        }
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(getNotificationAttributes(BASE_FLAGS)));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+        Mockito.reset(mCallback);
+
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            final StatusBarNotification sbn =
+                    getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, "app group");
+            sbn.setOverrideGroupKey("autogrouped");
+            mGroupHelper.onNotificationPosted(sbn, true);
+            verify(mCallback, times(1)).removeAutoGroup(sbn.getKey());
+            if (i < AUTOGROUP_AT_COUNT - 1) {
+                verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+            }
+        }
+        verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), anyString());
+    }
+
+    @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     public void testNewNotificationsAddedToAutogroup_ifOriginalNotificationsCanceled_alwaysGroup() {
         final String pkg = "package";
@@ -915,8 +989,9 @@
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
-    public void testAddSummary_diffIcon_diffColor() {
+    public void testAddSummary_diffIcon_diffColor_disableFlag() {
         final String pkg = "package";
         final Icon initialIcon = mock(Icon.class);
         when(initialIcon.sameAs(initialIcon)).thenReturn(true);
@@ -959,6 +1034,51 @@
     }
 
     @Test
+    @EnableFlags({Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE,
+            android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+    public void testAddSummary_diffIcon_diffColor() {
+        final String pkg = "package";
+        final Icon initialIcon = mock(Icon.class);
+        when(initialIcon.sameAs(initialIcon)).thenReturn(true);
+        final int initialIconColor = Color.BLUE;
+
+        // Spy GroupHelper for getMonochromeAppIcon
+        final Icon monochromeIcon = mock(Icon.class);
+        when(monochromeIcon.sameAs(monochromeIcon)).thenReturn(true);
+        GroupHelper groupHelper = spy(mGroupHelper);
+        doReturn(monochromeIcon).when(groupHelper).getMonochromeAppIcon(eq(pkg));
+
+        final NotificationAttributes initialAttr = new NotificationAttributes(BASE_FLAGS,
+                initialIcon, initialIconColor, DEFAULT_VISIBILITY);
+
+        // Add notifications with same icon and color
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            StatusBarNotification sbn = getSbn(pkg, i, String.valueOf(i), UserHandle.SYSTEM, null,
+                    initialIcon, initialIconColor);
+            groupHelper.onNotificationPosted(sbn, false);
+        }
+        // Check that the summary would have the same icon and color
+        verify(mCallback, times(1)).addAutoGroupSummary(
+                anyInt(), eq(pkg), anyString(), eq(initialAttr));
+        verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString());
+
+        // After auto-grouping, add new notification with a different color
+        final Icon newIcon = mock(Icon.class);
+        final int newIconColor = Color.YELLOW;
+        StatusBarNotification sbn = getSbn(pkg, AUTOGROUP_AT_COUNT,
+                String.valueOf(AUTOGROUP_AT_COUNT), UserHandle.SYSTEM, null, newIcon,
+                newIconColor);
+        groupHelper.onNotificationPosted(sbn, true);
+
+        // Summary should be updated to the default color and the icon to the monochrome icon
+        NotificationAttributes newAttr = new NotificationAttributes(BASE_FLAGS, monochromeIcon,
+                COLOR_DEFAULT, DEFAULT_VISIBILITY);
+        verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(), eq(newAttr));
+    }
+
+    @Test
     @DisableFlags(android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST)
     @EnableFlags(Flags.FLAG_AUTOGROUP_SUMMARY_ICON_UPDATE)
     public void testAddSummary_diffVisibility_alwaysAutogroup() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 4a9760b..e91fd37 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2726,6 +2726,19 @@
         assertNoStartingWindow(activity);
     }
 
+    @Test
+    public void testPostCleanupStartingWindow() {
+        registerTestStartingWindowOrganizer();
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        activity.addStartingWindow(mPackageName, android.R.style.Theme, null, true, true, false,
+                true, false, false, false);
+        waitUntilHandlersIdle();
+        assertHasStartingWindow(activity);
+        // Simulate Shell remove starting window actively.
+        activity.mStartingWindow.removeImmediately();
+        assertNoStartingWindow(activity);
+    }
+
     private void testLegacySplashScreen(int targetSdk, int verifyType) {
         final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         activity.mTargetSdk = targetSdk;
diff --git a/telephony/java/android/telephony/CarrierRestrictionRules.java b/telephony/java/android/telephony/CarrierRestrictionRules.java
index d5db612..65e8e13 100644
--- a/telephony/java/android/telephony/CarrierRestrictionRules.java
+++ b/telephony/java/android/telephony/CarrierRestrictionRules.java
@@ -555,10 +555,11 @@
          * Set the device's carrier restriction status
          *
          * @param carrierRestrictionStatus device restriction status
-         * @hide
          */
         public @NonNull
-        Builder setCarrierRestrictionStatus(int carrierRestrictionStatus) {
+        @FlaggedApi(Flags.FLAG_SET_CARRIER_RESTRICTION_STATUS)
+        Builder setCarrierRestrictionStatus(
+                @CarrierRestrictionStatus int carrierRestrictionStatus) {
             mRules.mCarrierRestrictionStatus = carrierRestrictionStatus;
             return this;
         }
diff --git a/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.aidl b/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.aidl
new file mode 100644
index 0000000..ecd248c
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.telephony.satellite;
+
+ parcelable SystemSelectionSpecifier;
diff --git a/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.java b/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.java
new file mode 100644
index 0000000..8a5e7f2
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/SystemSelectionSpecifier.java
@@ -0,0 +1,175 @@
+/*
+ * 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.telephony.satellite;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.IntArray;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class SystemSelectionSpecifier implements Parcelable {
+
+    /** Network plmn associated with channel information. */
+    @NonNull private String mMccMnc;
+
+    /** The frequency bands to scan. Maximum length of the vector is 8. */
+    @NonNull private IntArray mBands;
+
+    /**
+     * The radio channels to scan as defined in 3GPP TS 25.101 and 36.101.
+     * Maximum length of the vector is 32.
+     */
+    @NonNull private IntArray mEarfcs;
+
+    /**
+     * @hide
+     */
+    public SystemSelectionSpecifier(@NonNull String mccmnc, @NonNull IntArray bands,
+            @NonNull IntArray earfcs) {
+        mMccMnc = mccmnc;
+        mBands = bands;
+        mEarfcs = earfcs;
+    }
+
+    private SystemSelectionSpecifier(Parcel in) {
+        readFromParcel(in);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        mMccMnc = TextUtils.emptyIfNull(mMccMnc);
+        out.writeString8(mMccMnc);
+
+        if (mBands != null && mBands.size() > 0) {
+            out.writeInt(mBands.size());
+            for (int i = 0; i < mBands.size(); i++) {
+                out.writeInt(mBands.get(i));
+            }
+        } else {
+            out.writeInt(0);
+        }
+
+        if (mEarfcs != null && mEarfcs.size() > 0) {
+            out.writeInt(mEarfcs.size());
+            for (int i = 0; i < mEarfcs.size(); i++) {
+                out.writeInt(mEarfcs.get(i));
+            }
+        } else {
+            out.writeInt(0);
+        }
+    }
+
+    @NonNull public static final Parcelable.Creator<SystemSelectionSpecifier> CREATOR =
+            new Creator<>() {
+                @Override
+                public SystemSelectionSpecifier createFromParcel(Parcel in) {
+                    return new SystemSelectionSpecifier(in);
+                }
+
+                @Override
+                public SystemSelectionSpecifier[] newArray(int size) {
+                    return new SystemSelectionSpecifier[size];
+                }
+            };
+
+    @Override
+    @NonNull public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("mccmnc:");
+        sb.append(mMccMnc);
+        sb.append(",");
+
+        sb.append("bands:");
+        if (mBands != null && mBands.size() > 0) {
+            for (int i = 0; i < mBands.size(); i++) {
+                sb.append(mBands.get(i));
+                sb.append(",");
+            }
+        } else {
+            sb.append("none,");
+        }
+
+        sb.append("earfcs:");
+        if (mEarfcs != null && mEarfcs.size() > 0) {
+            for (int i = 0; i < mEarfcs.size(); i++) {
+                sb.append(mEarfcs.get(i));
+                sb.append(",");
+            }
+        } else {
+            sb.append("none");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SystemSelectionSpecifier that = (SystemSelectionSpecifier) o;
+        return Objects.equals(mMccMnc, that.mMccMnc)
+                && Objects.equals(mBands, that.mBands)
+                && Objects.equals(mEarfcs, that.mEarfcs);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMccMnc, mBands, mEarfcs);
+    }
+
+    @NonNull public String getMccMnc() {
+        return mMccMnc;
+    }
+
+    @NonNull public IntArray getBands() {
+        return mBands;
+    }
+
+    @NonNull public IntArray getEarfcs() {
+        return mEarfcs;
+    }
+
+    private void readFromParcel(Parcel in) {
+        mMccMnc = in.readString();
+
+        mBands = new IntArray();
+        int numBands = in.readInt();
+        if (numBands > 0) {
+            for (int i = 0; i < numBands; i++) {
+                mBands.add(in.readInt());
+            }
+        }
+
+        mEarfcs = new IntArray();
+        int numEarfcs = in.readInt();
+        if (numEarfcs > 0) {
+            for (int i = 0; i < numEarfcs; i++) {
+                mEarfcs.add(in.readInt());
+            }
+        }
+    }
+}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 16983a0..b82396e 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -23,6 +23,7 @@
 import android.telephony.satellite.stub.ISatelliteCapabilitiesConsumer;
 import android.telephony.satellite.stub.ISatelliteListener;
 import android.telephony.satellite.stub.SatelliteDatagram;
+import android.telephony.satellite.stub.SystemSelectionSpecifier;
 
 /**
  * {@hide}
@@ -500,4 +501,21 @@
       *   SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
       */
      void abortSendingSatelliteDatagrams(in IIntegerConsumer resultCallback);
+
+     /**
+      * Request to update the satellite subscription to be used for Non-Terrestrial network.
+      *
+      * @param iccId The ICCID of the subscription
+      * @param resultCallback The callback to receive the error code result of the operation.
+      */
+     void updateSatelliteSubscription(in String iccId, in IIntegerConsumer resultCallback);
+
+     /**
+      * Request to update system selection channels
+      *
+      * @param systemSelectionSpecifiers list of system selection specifiers
+      * @param resultCallback The callback to receive the error code result of the operation.
+      */
+     void updateSystemSelectionChannels(in List<SystemSelectionSpecifier> systemSelectionSpecifiers,
+            in IIntegerConsumer resultCallback);
 }
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index a623633..d8b4974 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -262,6 +262,22 @@
                     "abortSendingSatelliteDatagrams");
         }
 
+        @Override
+        public void updateSatelliteSubscription(String iccId, IIntegerConsumer resultCallback)
+                throws RemoteException {
+            executeMethodAsync(() -> SatelliteImplBase.this.updateSatelliteSubscription(
+                    iccId, resultCallback), "updateSatelliteSubscription");
+        }
+
+        @Override
+        public void updateSystemSelectionChannels(
+                List<SystemSelectionSpecifier> systemSelectionSpecifiers,
+                IIntegerConsumer resultCallback) throws RemoteException {
+            executeMethodAsync(() -> SatelliteImplBase.this.updateSystemSelectionChannels(
+                    systemSelectionSpecifiers, resultCallback),
+                    "updateSystemSelectionChannels");
+        }
+
         // Call the methods with a clean calling identity on the executor and wait indefinitely for
         // the future to return.
         private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException {
@@ -768,4 +784,27 @@
     public void abortSendingSatelliteDatagrams(@NonNull IIntegerConsumer resultCallback){
         // stub implementation
     }
+
+    /**
+     * Request to update the satellite subscription to be used for Non-Terrestrial network.
+     *
+     * @param iccId The ICCID of the subscription
+     * @param resultCallback The callback to receive the error code result of the operation.
+     */
+    public void updateSatelliteSubscription(String iccId,
+            @NonNull IIntegerConsumer resultCallback) {
+        // stub implementation
+    }
+
+    /**
+     * Request to update system selection channels
+     *
+     * @param systemSelectionSpecifiers list of system selection specifiers
+     * @param resultCallback The callback to receive the error code result of the operation.
+     */
+    public void updateSystemSelectionChannels(
+            @NonNull List<SystemSelectionSpecifier> systemSelectionSpecifiers,
+            @NonNull IIntegerConsumer resultCallback) {
+        // stub implementation
+    }
 }
diff --git a/telephony/java/android/telephony/satellite/stub/SystemSelectionSpecifier.aidl b/telephony/java/android/telephony/satellite/stub/SystemSelectionSpecifier.aidl
new file mode 100644
index 0000000..22240f6
--- /dev/null
+++ b/telephony/java/android/telephony/satellite/stub/SystemSelectionSpecifier.aidl
@@ -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 android.telephony.satellite.stub;
+
+/**
+ * {@hide}
+ */
+parcelable SystemSelectionSpecifier {
+    /** Network plmn associated with channel information. */
+    String mMccMnc;
+
+    /**
+     * The frequency bands to scan. Bands and earfcns won't overlap.
+     * Bands will be filled only if the whole band is needed.
+     * Maximum length of the vector is 8.
+     */
+    int[] mBands;
+
+    /**
+     * The radio channels to scan as defined in 3GPP TS 25.101 and 36.101.
+     * Maximum length of the vector is 32.
+     */
+    int[] mEarfcs;
+}
diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp
index 2cdf542..e09fbf6 100644
--- a/tests/FlickerTests/ActivityEmbedding/Android.bp
+++ b/tests/FlickerTests/ActivityEmbedding/Android.bp
@@ -20,17 +20,65 @@
     // all of the 'license_kinds' from "frameworks_base_license"
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
+    default_team: "trendy_team_windowing_sdk",
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-android_test {
-    name: "FlickerTestsOther",
+filegroup {
+    name: "FlickerTestsOtherCommon-src",
+    srcs: ["src/**/ActivityEmbeddingTestBase.kt"],
+}
+
+filegroup {
+    name: "FlickerTestsOtherOpen-src",
+    srcs: ["src/**/open/*"],
+}
+
+filegroup {
+    name: "FlickerTestsOtherRotation-src",
+    srcs: ["src/**/rotation/*"],
+}
+
+java_library {
+    name: "FlickerTestsOtherCommon",
+    defaults: ["FlickerTestsDefault"],
+    srcs: [":FlickerTestsOtherCommon-src"],
+    static_libs: ["FlickerTestsBase"],
+}
+
+java_defaults {
+    name: "FlickerTestsOtherDefaults",
     defaults: ["FlickerTestsDefault"],
     manifest: "AndroidManifest.xml",
     package_name: "com.android.server.wm.flicker",
     instrumentation_target_package: "com.android.server.wm.flicker",
     test_config_template: "AndroidTestTemplate.xml",
-    srcs: ["src/**/*"],
-    static_libs: ["FlickerTestsBase"],
+    static_libs: [
+        "FlickerTestsBase",
+        "FlickerTestsOtherCommon",
+    ],
     data: ["trace_config/*"],
 }
+
+android_test {
+    name: "FlickerTestsOtherOpen",
+    defaults: ["FlickerTestsOtherDefaults"],
+    srcs: [":FlickerTestsOtherOpen-src"],
+}
+
+android_test {
+    name: "FlickerTestsOtherRotation",
+    defaults: ["FlickerTestsOtherDefaults"],
+    srcs: [":FlickerTestsOtherRotation-src"],
+}
+
+android_test {
+    name: "FlickerTestsOther",
+    defaults: ["FlickerTestsOtherDefaults"],
+    srcs: ["src/**/*"],
+    exclude_srcs: [
+        ":FlickerTestsOtherOpen-src",
+        ":FlickerTestsOtherRotation-src",
+        ":FlickerTestsOtherCommon-src",
+    ],
+}
diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt
index 8a241de..209a14b 100644
--- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt
+++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/Utils.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker.service
 
 import android.app.Instrumentation
+import android.platform.test.rule.DisableNotificationCooldownSettingRule
 import android.platform.test.rule.NavigationModeRule
 import android.platform.test.rule.PressHomeRule
 import android.platform.test.rule.UnlockScreenRule
@@ -48,6 +49,7 @@
                     clearCacheAfterParsing = false
                 )
             )
+            .around(DisableNotificationCooldownSettingRule())
             .around(PressHomeRule())
     }
 }
diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp
index 3538949c..ccc3683 100644
--- a/tests/FlickerTests/IME/Android.bp
+++ b/tests/FlickerTests/IME/Android.bp
@@ -39,6 +39,10 @@
     defaults: ["FlickerTestsDefault"],
     manifest: "AndroidManifest.xml",
     test_config_template: "AndroidTestTemplate.xml",
+    test_suites: [
+        "device-tests",
+        "device-platinum-tests",
+    ],
     srcs: ["src/**/*"],
     static_libs: ["FlickerTestsBase"],
     data: ["trace_config/*"],
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
index dc50135..ed6e8df 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt
@@ -23,6 +23,7 @@
 import android.tools.flicker.legacy.LegacyFlickerTest
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
 import org.junit.FixMethodOrder
@@ -77,6 +78,7 @@
 
     @Presubmit @Test fun imeLayerBecomesInvisible() = flicker.imeLayerBecomesInvisible()
 
+    @FlakyTest(bugId = 330486656)
     @Presubmit
     @Test
     fun imeAppLayerIsAlwaysVisible() {
diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
index c29e71c..07fc230 100644
--- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
+++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
@@ -18,6 +18,7 @@
 
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
+import android.platform.test.rule.DisableNotificationCooldownSettingRule
 import android.tools.flicker.junit.FlickerParametersRunnerFactory
 import android.tools.flicker.legacy.FlickerBuilder
 import android.tools.flicker.legacy.FlickerTestData
@@ -37,6 +38,7 @@
 import com.android.server.wm.flicker.taskBarLayerIsVisibleAtEnd
 import com.android.server.wm.flicker.taskBarWindowIsVisibleAtEnd
 import org.junit.Assume
+import org.junit.ClassRule
 import org.junit.FixMethodOrder
 import org.junit.Ignore
 import org.junit.Test
@@ -208,5 +210,10 @@
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
         fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+
+        /** Ensures that posted notifications will alert and HUN even just after boot. */
+        @ClassRule
+        @JvmField
+        val disablenotificationCooldown = DisableNotificationCooldownSettingRule()
     }
 }
diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp
index a85d809..c0cbdc3 100644
--- a/tests/Input/Android.bp
+++ b/tests/Input/Android.bp
@@ -31,6 +31,7 @@
         "androidx.test.runner",
         "androidx.test.uiautomator_uiautomator",
         "compatibility-device-util-axt",
+        "cts-input-lib",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt
index 4893d14..6b95f5c 100644
--- a/tests/Input/src/com/android/test/input/AnrTest.kt
+++ b/tests/Input/src/com/android/test/input/AnrTest.kt
@@ -21,21 +21,23 @@
 
 import android.app.ActivityManager
 import android.app.ApplicationExitInfo
+import android.content.Context
 import android.graphics.Rect
+import android.hardware.display.DisplayManager
 import android.os.Build
 import android.os.IInputConstants.UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS
 import android.os.SystemClock
 import android.provider.Settings
 import android.provider.Settings.Global.HIDE_ERROR_DIALOGS
 import android.testing.PollingCheck
-import android.view.InputDevice
-import android.view.MotionEvent
 
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.UiObject2
 import androidx.test.uiautomator.Until
 
+import com.android.cts.input.UinputTouchScreen
+
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
@@ -150,6 +152,18 @@
         assertEquals(ApplicationExitInfo.REASON_ANR, reasons[0].reason)
     }
 
+    private fun clickOnObject(obj: UiObject2) {
+        val displayManager =
+            instrumentation.context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        val display = displayManager.getDisplay(obj.getDisplayId())
+        val touchScreen = UinputTouchScreen(instrumentation, display)
+
+        val rect: Rect = obj.visibleBounds
+        val pointer = touchScreen.touchDown(rect.centerX(), rect.centerY())
+        pointer.lift()
+        touchScreen.close()
+    }
+
     private fun triggerAnr() {
         startUnresponsiveActivity()
         val uiDevice: UiDevice = UiDevice.getInstance(instrumentation)
@@ -160,13 +174,7 @@
             return
         }
 
-        val rect: Rect = obj.visibleBounds
-        val downTime = SystemClock.uptimeMillis()
-        val downEvent = MotionEvent.obtain(downTime, downTime,
-                MotionEvent.ACTION_DOWN, rect.left.toFloat(), rect.top.toFloat(), 0 /* metaState */)
-        downEvent.source = InputDevice.SOURCE_TOUCHSCREEN
-
-        instrumentation.uiAutomation.injectInputEvent(downEvent, false /* sync*/)
+        clickOnObject(obj)
 
         SystemClock.sleep(DISPATCHING_TIMEOUT.toLong()) // default ANR timeout for gesture monitors
     }
diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
index a8b383c..093923f 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -101,8 +101,8 @@
     private static final String OBSERVER_NAME_2 = "observer2";
     private static final String OBSERVER_NAME_3 = "observer3";
     private static final String OBSERVER_NAME_4 = "observer4";
-    private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(10);
-    private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(50);
+    private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1);
+    private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5);
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -1453,8 +1453,7 @@
         raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A,
                 VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN);
 
-        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS
-                - TimeUnit.MINUTES.toMillis(1));
+        moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS);
 
         // The first failure will be outside the threshold.
         raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A,
@@ -1713,9 +1712,6 @@
             watchdog.onPackageFailure(packages, failureReason);
         }
         mTestLooper.dispatchAll();
-        if (Flags.recoverabilityDetection()) {
-            moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS);
-        }
     }
 
     private PackageWatchdog createWatchdog() {
diff --git a/tests/TrustTests/AndroidManifest.xml b/tests/TrustTests/AndroidManifest.xml
index 30cf345..2f6c0dd 100644
--- a/tests/TrustTests/AndroidManifest.xml
+++ b/tests/TrustTests/AndroidManifest.xml
@@ -78,6 +78,7 @@
                 <action android:name="android.service.trust.TrustAgentService" />
             </intent-filter>
         </service>
+
         <service
             android:name=".IsActiveUnlockRunningTrustAgent"
             android:exported="true"
@@ -88,6 +89,16 @@
             </intent-filter>
         </service>
 
+        <service
+            android:name=".UnlockAttemptTrustAgent"
+            android:exported="true"
+            android:label="Test Agent"
+            android:permission="android.permission.BIND_TRUST_AGENT">
+            <intent-filter>
+                <action android:name="android.service.trust.TrustAgentService" />
+            </intent-filter>
+        </service>
+
     </application>
 
     <!--  self-instrumenting test package. -->
diff --git a/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt
new file mode 100644
index 0000000..2c9361d
--- /dev/null
+++ b/tests/TrustTests/src/android/trust/test/UnlockAttemptTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.trust.test
+
+import android.app.trust.TrustManager
+import android.content.Context
+import android.trust.BaseTrustAgentService
+import android.trust.TrustTestActivity
+import android.trust.test.lib.LockStateTrackingRule
+import android.trust.test.lib.ScreenLockRule
+import android.trust.test.lib.TestTrustListener
+import android.trust.test.lib.TrustAgentRule
+import android.util.Log
+import androidx.test.core.app.ApplicationProvider.getApplicationContext
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+
+/**
+ * Test for the impacts of reporting unlock attempts.
+ *
+ * atest TrustTests:UnlockAttemptTest
+ */
+@RunWith(AndroidJUnit4::class)
+class UnlockAttemptTest {
+    private val context = getApplicationContext<Context>()
+    private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager
+    private val userId = context.userId
+    private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java)
+    private val screenLockRule = ScreenLockRule(requireStrongAuth = true)
+    private val lockStateTrackingRule = LockStateTrackingRule()
+    private val trustAgentRule =
+        TrustAgentRule<UnlockAttemptTrustAgent>(startUnlocked = false, startEnabled = false)
+
+    private val trustListener = UnlockAttemptTrustListener()
+    private val agent get() = trustAgentRule.agent
+
+    @get:Rule
+    val rule: RuleChain =
+        RuleChain.outerRule(activityScenarioRule)
+            .around(screenLockRule)
+            .around(lockStateTrackingRule)
+            .around(trustAgentRule)
+
+    @Before
+    fun setUp() {
+        trustManager.registerTrustListener(trustListener)
+    }
+
+    @Test
+    fun successfulUnlockAttempt_allowsTrustAgentToStart() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) {
+            trustAgentRule.enableTrustAgent()
+
+            triggerSuccessfulUnlock()
+
+            trustAgentRule.verifyAgentIsRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START)
+        }
+
+    @Test
+    fun successfulUnlockAttempt_notifiesTrustAgent() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+            val oldSuccessfulCount = agent.successfulUnlockCallCount
+            val oldFailedCount = agent.failedUnlockCallCount
+
+            triggerSuccessfulUnlock()
+
+            assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount + 1)
+            assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount)
+        }
+
+    @Test
+    fun successfulUnlockAttempt_notifiesTrustListenerOfManagedTrust() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+            val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0
+
+            triggerSuccessfulUnlock()
+
+            assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo(
+                oldTrustManagedChangedCount + 1
+            )
+        }
+
+    @Test
+    fun failedUnlockAttempt_doesNotAllowTrustAgentToStart() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = false, managingTrust = false) {
+            trustAgentRule.enableTrustAgent()
+
+            triggerFailedUnlock()
+
+            trustAgentRule.ensureAgentIsNotRunning(MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START)
+        }
+
+    @Test
+    fun failedUnlockAttempt_notifiesTrustAgent() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+            val oldSuccessfulCount = agent.successfulUnlockCallCount
+            val oldFailedCount = agent.failedUnlockCallCount
+
+            triggerFailedUnlock()
+
+            assertThat(agent.successfulUnlockCallCount).isEqualTo(oldSuccessfulCount)
+            assertThat(agent.failedUnlockCallCount).isEqualTo(oldFailedCount + 1)
+        }
+
+    @Test
+    fun failedUnlockAttempt_doesNotNotifyTrustListenerOfManagedTrust() =
+        runUnlockAttemptTest(enableAndVerifyTrustAgent = true, managingTrust = true) {
+            val oldTrustManagedChangedCount = trustListener.onTrustManagedChangedCount[userId] ?: 0
+
+            triggerFailedUnlock()
+
+            assertThat(trustListener.onTrustManagedChangedCount[userId] ?: 0).isEqualTo(
+                oldTrustManagedChangedCount
+            )
+        }
+
+    private fun runUnlockAttemptTest(
+        enableAndVerifyTrustAgent: Boolean,
+        managingTrust: Boolean,
+        testBlock: () -> Unit,
+    ) {
+        if (enableAndVerifyTrustAgent) {
+            Log.i(TAG, "Triggering successful unlock")
+            triggerSuccessfulUnlock()
+            Log.i(TAG, "Enabling and waiting for trust agent")
+            trustAgentRule.enableAndVerifyTrustAgentIsRunning(
+                MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START
+            )
+            Log.i(TAG, "Managing trust: $managingTrust")
+            agent.setManagingTrust(managingTrust)
+            await()
+        }
+        testBlock()
+    }
+
+    private fun triggerSuccessfulUnlock() {
+        screenLockRule.successfulScreenLockAttempt()
+        trustAgentRule.reportSuccessfulUnlock()
+        await()
+    }
+
+    private fun triggerFailedUnlock() {
+        screenLockRule.failedScreenLockAttempt()
+        trustAgentRule.reportFailedUnlock()
+        await()
+    }
+
+    companion object {
+        private const val TAG = "UnlockAttemptTest"
+        private fun await(millis: Long = 500) = Thread.sleep(millis)
+        private const val MAX_WAIT_FOR_ENABLED_TRUST_AGENT_TO_START = 10000L
+    }
+}
+
+class UnlockAttemptTrustAgent : BaseTrustAgentService() {
+    var successfulUnlockCallCount: Long = 0
+        private set
+    var failedUnlockCallCount: Long = 0
+        private set
+
+    override fun onUnlockAttempt(successful: Boolean) {
+        super.onUnlockAttempt(successful)
+        if (successful) {
+            successfulUnlockCallCount++
+        } else {
+            failedUnlockCallCount++
+        }
+    }
+}
+
+private class UnlockAttemptTrustListener : TestTrustListener() {
+    var enabledTrustAgentsChangedCount = mutableMapOf<Int, Int>()
+    var onTrustManagedChangedCount = mutableMapOf<Int, Int>()
+
+    override fun onEnabledTrustAgentsChanged(userId: Int) {
+        enabledTrustAgentsChangedCount.compute(userId) { _: Int, curr: Int? ->
+            if (curr == null) 0 else curr + 1
+        }
+    }
+
+    data class TrustChangedParams(
+        val enabled: Boolean,
+        val newlyUnlocked: Boolean,
+        val userId: Int,
+        val flags: Int,
+        val trustGrantedMessages: MutableList<String>?
+    )
+
+    val onTrustChangedCalls = mutableListOf<TrustChangedParams>()
+
+    override fun onTrustChanged(
+        enabled: Boolean,
+        newlyUnlocked: Boolean,
+        userId: Int,
+        flags: Int,
+        trustGrantedMessages: MutableList<String>
+    ) {
+        onTrustChangedCalls += TrustChangedParams(
+            enabled, newlyUnlocked, userId, flags, trustGrantedMessages
+        )
+    }
+
+    override fun onTrustManagedChanged(enabled: Boolean, userId: Int) {
+        onTrustManagedChangedCount.compute(userId) { _: Int, curr: Int? ->
+            if (curr == null) 0 else curr + 1
+        }
+    }
+}
diff --git a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
index f1edca3..1ccdcc6 100644
--- a/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/ScreenLockRule.kt
@@ -24,6 +24,8 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import androidx.test.uiautomator.UiDevice
 import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
 import com.android.internal.widget.LockscreenCredential
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.rules.TestRule
@@ -32,13 +34,18 @@
 
 /**
  * Sets a screen lock on the device for the duration of the test.
+ *
+ * @param requireStrongAuth Whether a strong auth is required at the beginning.
+ * If true, trust agents will not be available until the user verifies their credentials.
  */
-class ScreenLockRule : TestRule {
+class ScreenLockRule(val requireStrongAuth: Boolean = false) : TestRule {
     private val context: Context = getApplicationContext()
+    private val userId = context.userId
     private val uiDevice = UiDevice.getInstance(getInstrumentation())
     private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService())
     private val lockPatternUtils = LockPatternUtils(context)
     private var instantLockSavedValue = false
+    private var strongAuthSavedValue: Int = 0
 
     override fun apply(base: Statement, description: Description) = object : Statement() {
         override fun evaluate() {
@@ -46,10 +53,12 @@
             dismissKeyguard()
             setScreenLock()
             setLockOnPowerButton()
+            configureStrongAuthState()
 
             try {
                 base.evaluate()
             } finally {
+                restoreStrongAuthState()
                 removeScreenLock()
                 revertLockOnPowerButton()
                 dismissKeyguard()
@@ -57,6 +66,22 @@
         }
     }
 
+    private fun configureStrongAuthState() {
+        strongAuthSavedValue = lockPatternUtils.getStrongAuthForUser(userId)
+        if (requireStrongAuth) {
+            Log.d(TAG, "Triggering strong auth due to simulated lockdown")
+            lockPatternUtils.requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, userId)
+            wait("strong auth required after lockdown") {
+                lockPatternUtils.getStrongAuthForUser(userId) ==
+                        STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN
+            }
+        }
+    }
+
+    private fun restoreStrongAuthState() {
+        lockPatternUtils.requireStrongAuth(strongAuthSavedValue, userId)
+    }
+
     private fun verifyNoScreenLockAlreadySet() {
         assertWithMessage("Screen Lock must not already be set on device")
                 .that(lockPatternUtils.isSecure(context.userId))
@@ -82,6 +107,22 @@
         }
     }
 
+    fun successfulScreenLockAttempt() {
+        lockPatternUtils.verifyCredential(LockscreenCredential.createPin(PIN), context.userId, 0)
+        lockPatternUtils.userPresent(context.userId)
+        wait("strong auth not required") {
+            lockPatternUtils.getStrongAuthForUser(context.userId) == STRONG_AUTH_NOT_REQUIRED
+        }
+    }
+
+    fun failedScreenLockAttempt() {
+        lockPatternUtils.verifyCredential(
+            LockscreenCredential.createPin(WRONG_PIN),
+            context.userId,
+            0
+        )
+    }
+
     private fun setScreenLock() {
         lockPatternUtils.setLockCredential(
                 LockscreenCredential.createPin(PIN),
@@ -121,5 +162,6 @@
     companion object {
         private const val TAG = "ScreenLockRule"
         private const val PIN = "0000"
+        private const val WRONG_PIN = "0001"
     }
 }
diff --git a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
index 18bc029..404c6d9 100644
--- a/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/TrustAgentRule.kt
@@ -20,14 +20,15 @@
 import android.content.ComponentName
 import android.content.Context
 import android.trust.BaseTrustAgentService
+import android.trust.test.lib.TrustAgentRule.Companion.invoke
 import android.util.Log
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import com.android.internal.widget.LockPatternUtils
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.KClass
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
-import kotlin.reflect.KClass
 
 /**
  * Enables a trust agent and causes the system service to bind to it.
@@ -37,7 +38,9 @@
  * @constructor Creates the rule. Do not use; instead, use [invoke].
  */
 class TrustAgentRule<T : BaseTrustAgentService>(
-    private val serviceClass: KClass<T>
+    private val serviceClass: KClass<T>,
+    private val startUnlocked: Boolean,
+    private val startEnabled: Boolean,
 ) : TestRule {
     private val context: Context = getApplicationContext()
     private val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager
@@ -48,11 +51,18 @@
     override fun apply(base: Statement, description: Description) = object : Statement() {
         override fun evaluate() {
             verifyTrustServiceRunning()
-            unlockDeviceWithCredential()
-            enableTrustAgent()
+            if (startUnlocked) {
+                reportSuccessfulUnlock()
+            } else {
+                Log.i(TAG, "Trust manager not starting in unlocked state")
+            }
 
             try {
-                verifyAgentIsRunning()
+                if (startEnabled) {
+                    enableAndVerifyTrustAgentIsRunning()
+                } else {
+                    Log.i(TAG, "Trust agent ${serviceClass.simpleName} not enabled")
+                }
                 base.evaluate()
             } finally {
                 disableTrustAgent()
@@ -64,12 +74,22 @@
         assertWithMessage("Trust service is not running").that(trustManager).isNotNull()
     }
 
-    private fun unlockDeviceWithCredential() {
-        Log.d(TAG, "Unlocking device with credential")
+    fun reportSuccessfulUnlock() {
+        Log.i(TAG, "Reporting successful unlock")
         trustManager.reportUnlockAttempt(true, context.userId)
     }
 
-    private fun enableTrustAgent() {
+    fun reportFailedUnlock() {
+        Log.i(TAG, "Reporting failed unlock")
+        trustManager.reportUnlockAttempt(false, context.userId)
+    }
+
+    fun enableAndVerifyTrustAgentIsRunning(maxWait: Long = 30000L) {
+        enableTrustAgent()
+        verifyAgentIsRunning(maxWait)
+    }
+
+    fun enableTrustAgent() {
         val componentName = ComponentName(context, serviceClass.java)
         val userId = context.userId
         Log.i(TAG, "Enabling trust agent ${componentName.flattenToString()} for user $userId")
@@ -79,12 +99,18 @@
         lockPatternUtils.setEnabledTrustAgents(agents, userId)
     }
 
-    private fun verifyAgentIsRunning() {
-        wait("${serviceClass.simpleName} to be running") {
+    fun verifyAgentIsRunning(maxWait: Long = 30000L) {
+        wait("${serviceClass.simpleName} to be running", maxWait) {
             BaseTrustAgentService.instance(serviceClass) != null
         }
     }
 
+    fun ensureAgentIsNotRunning(window: Long = 30000L) {
+        ensure("${serviceClass.simpleName} is not running", window) {
+            BaseTrustAgentService.instance(serviceClass) == null
+        }
+    }
+
     private fun disableTrustAgent() {
         val componentName = ComponentName(context, serviceClass.java)
         val userId = context.userId
@@ -97,13 +123,23 @@
 
     companion object {
         /**
-         * Creates a new rule for the specified agent class. Example usage:
+         * Creates a new rule for the specified agent class. Starts with the device unlocked and
+         * the trust agent enabled. Example usage:
          * ```
          *   @get:Rule val rule = TrustAgentRule<MyTestAgent>()
          * ```
+         *
+         * Also supports setting different device lock and trust agent enablement states:
+         * ```
+         *   @get:Rule val rule = TrustAgentRule<MyTestAgent>(startUnlocked = false, startEnabled = false)
+         * ```
          */
-        inline operator fun <reified T : BaseTrustAgentService> invoke() =
-            TrustAgentRule(T::class)
+        inline operator fun <reified T : BaseTrustAgentService> invoke(
+            startUnlocked: Boolean = true,
+            startEnabled: Boolean = true,
+        ) =
+            TrustAgentRule(T::class, startUnlocked, startEnabled)
+
 
         private const val TAG = "TrustAgentRule"
     }
diff --git a/tests/TrustTests/src/android/trust/test/lib/utils.kt b/tests/TrustTests/src/android/trust/test/lib/Utils.kt
similarity index 63%
rename from tests/TrustTests/src/android/trust/test/lib/utils.kt
rename to tests/TrustTests/src/android/trust/test/lib/Utils.kt
index e047202..3b32b47 100644
--- a/tests/TrustTests/src/android/trust/test/lib/utils.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/Utils.kt
@@ -39,7 +39,7 @@
 ) {
     var waited = 0L
     var count = 0
-    while (!conditionFunction.invoke(count)) {
+    while (!conditionFunction(count)) {
         assertWithMessage("Condition exceeded maximum wait time of $maxWait ms: $description")
             .that(waited <= maxWait)
             .isTrue()
@@ -49,3 +49,34 @@
         Thread.sleep(rate)
     }
 }
+
+/**
+ * Ensures that [conditionFunction] is true with a failed assertion if it is not within [window]
+ * ms.
+ *
+ * The condition function can perform additional logic (for example, logging or attempting to make
+ * the condition become true).
+ *
+ * @param conditionFunction function which takes the attempt count & returns whether the condition
+ *                          is met
+ */
+internal fun ensure(
+    description: String? = null,
+    window: Long = 30000L,
+    rate: Long = 50L,
+    conditionFunction: (count: Int) -> Boolean
+) {
+    var waited = 0L
+    var count = 0
+    while (waited <= window) {
+        assertWithMessage("Condition failed within $window ms: $description").that(
+                conditionFunction(
+                    count
+                )
+            ).isTrue()
+        waited += rate
+        count++
+        Log.i(TAG, "Ensuring $description ($waited/$window) #$count")
+        Thread.sleep(rate)
+    }
+}