Merge "Disambiguate between unique and original ids in MR2Provider" into main
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/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 329fb00..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;
@@ -3262,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) {
@@ -8566,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/servertransaction/WindowStateInsetsControlChangeItem.java b/core/java/android/app/servertransaction/WindowStateInsetsControlChangeItem.java
index ed18a05..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);
     }
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/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/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/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java
index 588e9e0..487214c 100644
--- a/core/java/android/view/InsetsSourceControl.java
+++ b/core/java/android/view/InsetsSourceControl.java
@@ -305,6 +305,18 @@
             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) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b54e052..496e899 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2294,12 +2294,8 @@
         mInsetsController.onStateChanged(insetsState);
         if (mAdded) {
             mInsetsController.onControlsChanged(controls);
-        } else if (controls != null) {
-            for (InsetsSourceControl control : controls) {
-                if (control != null) {
-                    control.release(SurfaceControl::release);
-                }
-            }
+        } else {
+            activeControls.release();
         }
     }
 
@@ -11306,6 +11302,9 @@
             mIsFromTransactionItem = false;
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (viewAncestor == null) {
+                if (isFromInsetsControlChangeItem) {
+                    activeControls.release();
+                }
                 return;
             }
             if (insetsState.isSourceOrDefaultVisible(ID_IME, Type.ime())) {
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/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/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/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/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/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/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/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/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/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/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt
index b43b5f3..373b3e8 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,
@@ -239,7 +238,9 @@
     } 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? {
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..39f2fce 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/CreateCredentialEvent.kt
@@ -17,6 +17,7 @@
 
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
 
 enum class CreateCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum {
 
@@ -52,7 +53,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(RESERVE_NEW_UI_EVENT_ID);
 
     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..89fd72c 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/logging/GetCredentialEvent.kt
@@ -17,6 +17,7 @@
 
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID
 
 enum class GetCredentialEvent(private val id: Int) : UiEventLogger.UiEventEnum {
 
@@ -54,7 +55,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(RESERVE_NEW_UI_EVENT_ID);
 
     override fun getId(): Int {
         return this.id
diff --git a/packages/SettingsLib/Color/res/values/colors.xml b/packages/SettingsLib/Color/res/values/colors.xml
index ef0dd1b..b0b9b10 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>
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..0447ef8 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -56,9 +56,6 @@
                 ".black",
                 android.R.color.white);
         map.put(
-                ".blue200",
-                R.color.settingslib_color_blue700);
-        map.put(
                 ".blue400",
                 R.color.settingslib_color_blue600);
         map.put(
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index c8992c3..04922d6 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -64,7 +64,6 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.InputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.file.Files;
@@ -86,13 +85,13 @@
 // FOR ACONFIGD TEST MISSION AND ROLLOUT
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
+import android.net.LocalSocketAddress;
+import android.net.LocalSocket;
 import android.util.proto.ProtoInputStream;
 import android.aconfigd.Aconfigd.StorageRequestMessage;
 import android.aconfigd.Aconfigd.StorageRequestMessages;
 import android.aconfigd.Aconfigd.StorageReturnMessage;
 import android.aconfigd.Aconfigd.StorageReturnMessages;
-import android.aconfigd.AconfigdClientSocket;
-import android.aconfigd.AconfigdFlagInfo;
 import android.aconfigd.AconfigdJavaUtils;
 import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
 /**
@@ -266,10 +265,6 @@
     @NonNull
     private Map<String, Map<String, String>> mNamespaceDefaults;
 
-    // TOBO(b/312444587): remove the comparison logic after Test Mission 2.
-    @NonNull
-    private Map<String, AconfigdFlagInfo> mAconfigDefaultFlags;
-
     public static final int SETTINGS_TYPE_GLOBAL = 0;
     public static final int SETTINGS_TYPE_SYSTEM = 1;
     public static final int SETTINGS_TYPE_SECURE = 2;
@@ -339,13 +334,8 @@
                 + settingTypeToString(getTypeFromKey(key)) + "]";
     }
 
-    public SettingsState(
-            Context context,
-            Object lock,
-            File file,
-            int key,
-            int maxBytesPerAppPackage,
-            Looper looper) {
+    public SettingsState(Context context, Object lock, File file, int key,
+            int maxBytesPerAppPackage, Looper looper) {
         // It is important that we use the same lock as the settings provider
         // to ensure multiple mutations on this state are atomically persisted
         // as the async persistence should be blocked while we make changes.
@@ -363,15 +353,12 @@
             mPackageToMemoryUsage = null;
         }
 
-        mHistoricalOperations =
-                Build.IS_DEBUGGABLE ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null;
+        mHistoricalOperations = Build.IS_DEBUGGABLE
+                ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null;
 
         mNamespaceDefaults = new HashMap<>();
-        mAconfigDefaultFlags = new HashMap<>();
 
         ProtoOutputStream requests = null;
-        Map<String, AconfigdFlagInfo> aconfigFlagMap = new HashMap<>();
-
         synchronized (mLock) {
             readStateSyncLocked();
 
@@ -388,114 +375,39 @@
                 }
             }
 
-            if (enableAconfigStorageDaemon()) {
-                if (isConfigSettingsKey(mKey)) {
-                    aconfigFlagMap = getAllAconfigFlagsFromSettings();
-                }
-            }
-
             if (isConfigSettingsKey(mKey)) {
-                requests = handleBulkSyncToNewStorage(aconfigFlagMap);
+                requests = handleBulkSyncToNewStorage();
             }
         }
 
-        if (enableAconfigStorageDaemon()) {
-            if (isConfigSettingsKey(mKey)){
-                AconfigdClientSocket localSocket = AconfigdJavaUtils.getAconfigdClientSocket();
-                if (requests != null) {
-                    InputStream res = localSocket.send(requests.getBytes());
-                    if (res == null) {
-                        Slog.w(LOG_TAG, "Bulk sync request to acongid failed.");
-                    }
-                }
-                // TOBO(b/312444587): remove the comparison logic after Test Mission 2.
-                if (mSettings.get("aconfigd_marker/bulk_synced").value.equals("true")
-                        && requests == null) {
-                    Map<String, AconfigdFlagInfo> aconfigdFlagMap =
-                            AconfigdJavaUtils.listFlagsValueInNewStorage(localSocket);
-                    compareFlagValueInNewStorage(
-                            aconfigFlagMap,
-                            mAconfigDefaultFlags,
-                            aconfigdFlagMap);
-                }
+        if (requests != null) {
+            LocalSocket client = new LocalSocket();
+            try{
+                client.connect(new LocalSocketAddress(
+                    "aconfigd", LocalSocketAddress.Namespace.RESERVED));
+                Slog.d(LOG_TAG, "connected to aconfigd socket");
+            } catch (IOException ioe) {
+                Slog.e(LOG_TAG, "failed to connect to aconfigd socket", ioe);
+                return;
             }
+            AconfigdJavaUtils.sendAconfigdRequests(client, requests);
         }
     }
 
-    // TOBO(b/312444587): remove the comparison logic after Test Mission 2.
-    public int compareFlagValueInNewStorage(
-            Map<String, AconfigdFlagInfo> settingFlagMap,
-            Map<String, AconfigdFlagInfo> defaultFlagMap,
-            Map<String, AconfigdFlagInfo> aconfigdFlagMap) {
-
-        // Get all defaults from the default map. The mSettings may not contain
-        // all flags, since it only contains updated flags.
-        int diffNum = 0;
-        for (Map.Entry<String, AconfigdFlagInfo> entry : defaultFlagMap.entrySet()) {
-            String key = entry.getKey();
-            AconfigdFlagInfo flag = entry.getValue();
-            if (settingFlagMap.containsKey(key)) {
-                flag.merge(settingFlagMap.get(key));
-            }
-
-            AconfigdFlagInfo aconfigdFlag = aconfigdFlagMap.get(key);
-            if (aconfigdFlag == null) {
-                Slog.w(LOG_TAG, String.format("Flag %s is missing from aconfigd", key));
-                diffNum++;
-                continue;
-            }
-            String diff = flag.dumpDiff(aconfigdFlag);
-            if (!diff.isEmpty()) {
-                Slog.w(
-                        LOG_TAG,
-                        String.format(
-                                "Flag %s is different in Settings and aconfig: %s", key, diff));
-                diffNum++;
-            }
-        }
-
-        for (String key : aconfigdFlagMap.keySet()) {
-            if (defaultFlagMap.containsKey(key)) continue;
-            Slog.w(LOG_TAG, String.format("Flag %s is missing from Settings", key));
-            diffNum++;
-        }
-
-        if (diffNum == 0) {
-            Slog.i(LOG_TAG, "Settings and new storage have same flags.");
-        }
-        return diffNum;
-    }
-
-    @GuardedBy("mLock")
-    public Map<String, AconfigdFlagInfo> getAllAconfigFlagsFromSettings() {
-        Map<String, AconfigdFlagInfo> ret = new HashMap<>();
-        int numSettings = mSettings.size();
-        int num_requests = 0;
-        for (int i = 0; i < numSettings; i++) {
-            String name = mSettings.keyAt(i);
-            Setting setting = mSettings.valueAt(i);
-            AconfigdFlagInfo flag =
-                    getFlagOverrideToSync(name, setting.getValue());
-            if (flag == null) {
-                continue;
-            }
-            String fullFlagName = flag.getFullFlagName();
-            AconfigdFlagInfo prev = ret.putIfAbsent(fullFlagName,flag);
-            if (prev != null) {
-                prev.merge(flag);
-            }
-            ++num_requests;
-        }
-        Slog.i(LOG_TAG, num_requests + " flag override requests created");
-        return ret;
+    // TODO(b/341764371): migrate aconfig flag push to GMS core
+    public static class FlagOverrideToSync {
+        public String packageName;
+        public String flagName;
+        public String flagValue;
+        public boolean isLocal;
     }
 
     // TODO(b/341764371): migrate aconfig flag push to GMS core
     @VisibleForTesting
     @GuardedBy("mLock")
-    public AconfigdFlagInfo getFlagOverrideToSync(String name, String value) {
+    public FlagOverrideToSync getFlagOverrideToSync(String name, String value) {
         int slashIdx = name.indexOf("/");
-        if (slashIdx <= 0 || slashIdx >= name.length() - 1) {
+        if (slashIdx <= 0 || slashIdx >= name.length()-1) {
             Slog.e(LOG_TAG, "invalid flag name " + name);
             return null;
         }
@@ -518,9 +430,8 @@
         }
 
         String aconfigName = namespace + "/" + fullFlagName;
-        boolean isAconfig =
-                mNamespaceDefaults.containsKey(namespace)
-                        && mNamespaceDefaults.get(namespace).containsKey(aconfigName);
+        boolean isAconfig = mNamespaceDefaults.containsKey(namespace)
+                            && mNamespaceDefaults.get(namespace).containsKey(aconfigName);
         if (!isAconfig) {
             return null;
         }
@@ -532,30 +443,25 @@
             return null;
         }
 
-        AconfigdFlagInfo.Builder builder = AconfigdFlagInfo.newBuilder()
-                        .setPackageName(fullFlagName.substring(0, dotIdx))
-                        .setFlagName(fullFlagName.substring(dotIdx + 1))
-                        .setDefaultFlagValue(mNamespaceDefaults.get(namespace).get(aconfigName));
-
-        if (isLocal) {
-            builder.setHasLocalOverride(isLocal).setBootFlagValue(value).setLocalFlagValue(value);
-        } else {
-            builder.setHasServerOverride(true).setServerFlagValue(value).setBootFlagValue(value);
-        }
-        return builder.build();
+        FlagOverrideToSync flag = new FlagOverrideToSync();
+        flag.packageName = fullFlagName.substring(0, dotIdx);
+        flag.flagName = fullFlagName.substring(dotIdx + 1);
+        flag.isLocal = isLocal;
+        flag.flagValue = value;
+        return flag;
     }
 
 
     // TODO(b/341764371): migrate aconfig flag push to GMS core
     @VisibleForTesting
     @GuardedBy("mLock")
-    public ProtoOutputStream handleBulkSyncToNewStorage(
-            Map<String, AconfigdFlagInfo> aconfigFlagMap) {
+    public ProtoOutputStream handleBulkSyncToNewStorage() {
         // get marker or add marker if it does not exist
         final String bulkSyncMarkerName = new String("aconfigd_marker/bulk_synced");
         Setting markerSetting = mSettings.get(bulkSyncMarkerName);
         if (markerSetting == null) {
-            markerSetting = new Setting(bulkSyncMarkerName, "false", false, "aconfig", "aconfig");
+            markerSetting = new Setting(
+                bulkSyncMarkerName, "false", false, "aconfig", "aconfig");
             mSettings.put(bulkSyncMarkerName, markerSetting);
         }
 
@@ -573,19 +479,24 @@
                 AconfigdJavaUtils.writeResetStorageRequest(requests);
 
                 // loop over all settings and add flag override requests
-                for (AconfigdFlagInfo flag : aconfigFlagMap.values()) {
-                    String value =
-                            flag.getHasLocalOverride()
-                                    ? flag.getLocalFlagValue()
-                                    : flag.getServerFlagValue();
+                final int numSettings = mSettings.size();
+                int num_requests = 0;
+                for (int i = 0; i < numSettings; i++) {
+                    String name = mSettings.keyAt(i);
+                    Setting setting = mSettings.valueAt(i);
+                    FlagOverrideToSync flag =
+                            getFlagOverrideToSync(name, setting.getValue());
+                    if (flag == null) {
+                        continue;
+                    }
+                    ++num_requests;
                     AconfigdJavaUtils.writeFlagOverrideRequest(
-                            requests,
-                            flag.getPackageName(),
-                            flag.getFlagName(),
-                            value,
-                            flag.getHasLocalOverride());
+                        requests, flag.packageName, flag.flagName, flag.flagValue,
+                        flag.isLocal);
                 }
 
+                Slog.i(LOG_TAG, num_requests + " flag override requests created");
+
                 // mark sync has been done
                 markerSetting.value = "true";
                 scheduleWriteIfNeededLocked();
@@ -602,14 +513,14 @@
                 return null;
             }
         }
+
     }
 
     @GuardedBy("mLock")
     private void loadAconfigDefaultValuesLocked(List<String> filePaths) {
         for (String fileName : filePaths) {
             try (FileInputStream inputStream = new FileInputStream(fileName)) {
-                loadAconfigDefaultValues(
-                        inputStream.readAllBytes(), mNamespaceDefaults, mAconfigDefaultFlags);
+                loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults);
             } catch (IOException e) {
                 Slog.e(LOG_TAG, "failed to read protobuf", e);
             }
@@ -655,30 +566,21 @@
 
     @VisibleForTesting
     @GuardedBy("mLock")
-    public static void loadAconfigDefaultValues(
-            byte[] fileContents,
-            @NonNull Map<String, Map<String, String>> defaultMap,
-            @NonNull Map<String, AconfigdFlagInfo> flagInfoDefault) {
+    public static void loadAconfigDefaultValues(byte[] fileContents,
+            @NonNull Map<String, Map<String, String>> defaultMap) {
         try {
-            parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
+            parsed_flags parsedFlags =
+                    parsed_flags.parseFrom(fileContents);
             for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
                 if (!defaultMap.containsKey(flag.getNamespace())) {
                     Map<String, String> defaults = new HashMap<>();
                     defaultMap.put(flag.getNamespace(), defaults);
                 }
-                String fullFlagName = flag.getPackage() + "." + flag.getName();
-                String flagName = flag.getNamespace() + "/" + fullFlagName;
-                String flagValue = flag.getState() == flag_state.ENABLED ? "true" : "false";
+                String flagName = flag.getNamespace()
+                        + "/" + flag.getPackage() + "." + flag.getName();
+                String flagValue = flag.getState() == flag_state.ENABLED
+                        ? "true" : "false";
                 defaultMap.get(flag.getNamespace()).put(flagName, flagValue);
-                if (!flagInfoDefault.containsKey(fullFlagName)) {
-                    flagInfoDefault.put(
-                            fullFlagName,
-                            AconfigdFlagInfo.newBuilder()
-                                    .setPackageName(flag.getPackage())
-                                    .setFlagName(flag.getName())
-                                    .setDefaultFlagValue(flagValue)
-                                    .build());
-                }
             }
         } catch (IOException e) {
             Slog.e(LOG_TAG, "failed to parse protobuf", e);
@@ -1744,6 +1646,7 @@
                         }
                     }
                 }
+
                 mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag,
                         fromSystem, id, isPreservedInRestore));
 
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index 256b999..244c8c4 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -24,13 +24,13 @@
 import android.aconfig.Aconfig;
 import android.aconfig.Aconfig.parsed_flag;
 import android.aconfig.Aconfig.parsed_flags;
-import android.aconfigd.AconfigdFlagInfo;
 import android.os.Looper;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Xml;
 import android.util.proto.ProtoOutputStream;
+import com.android.providers.settings.SettingsState.FlagOverrideToSync;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -145,32 +145,16 @@
                         .setState(Aconfig.flag_state.ENABLED)
                         .setPermission(Aconfig.flag_permission.READ_WRITE))
                 .build();
-        
-        AconfigdFlagInfo flag1 = AconfigdFlagInfo.newBuilder()
-                                                .setPackageName("com.android.flags")
-                                                .setFlagName("flag1")
-                                                .setDefaultFlagValue("false")
-                                                .build();
-        AconfigdFlagInfo flag2 = AconfigdFlagInfo.newBuilder()
-                                                .setPackageName("com.android.flags")
-                                                .setFlagName("flag2")
-                                                .setDefaultFlagValue("true")
-                                                .build();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
 
         synchronized (lock) {
             Map<String, Map<String, String>> defaults = new HashMap<>();
-            settingsState.loadAconfigDefaultValues(
-                flags.toByteArray(), defaults, flagInfoDefault);
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
             Map<String, String> namespaceDefaults = defaults.get("test_namespace");
             assertEquals(2, namespaceDefaults.keySet().size());
 
             assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1"));
             assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2"));
         }
-
-        assertEquals(flag1, flagInfoDefault.get(flag1.getFullFlagName()));
-        assertEquals(flag2, flagInfoDefault.get(flag2.getFullFlagName()));
     }
 
     @Test
@@ -181,8 +165,6 @@
                 InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey,
                 SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
 
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
-
         parsed_flags flags = parsed_flags
                 .newBuilder()
                 .addParsedFlag(parsed_flag
@@ -195,8 +177,7 @@
 
         synchronized (lock) {
             Map<String, Map<String, String>> defaults = new HashMap<>();
-            settingsState.loadAconfigDefaultValues(
-                flags.toByteArray(), defaults, flagInfoDefault);
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
 
             Map<String, String> namespaceDefaults = defaults.get("test_namespace");
             assertEquals(null, namespaceDefaults);
@@ -223,12 +204,10 @@
                         .setState(Aconfig.flag_state.DISABLED)
                         .setPermission(Aconfig.flag_permission.READ_WRITE))
                 .build();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
 
         synchronized (lock) {
             Map<String, Map<String, String>> defaults = new HashMap<>();
-            settingsState.loadAconfigDefaultValues(
-                flags.toByteArray(), defaults, flagInfoDefault);
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
             settingsState.addAconfigDefaultValuesFromMap(defaults);
 
             settingsState.insertSettingLocked("test_namespace/com.android.flags.flag5",
@@ -259,10 +238,8 @@
     @Test
     public void testInvalidAconfigProtoDoesNotCrash() {
         Map<String, Map<String, String>> defaults = new HashMap<>();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
         SettingsState settingsState = getSettingStateObject();
-        settingsState.loadAconfigDefaultValues(
-            "invalid protobuf".getBytes(), defaults, flagInfoDefault);
+        settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults);
     }
 
     @Test
@@ -782,8 +759,6 @@
         Map<String, String> keyValues =
                 Map.of("test_namespace/com.android.flags.flag3", "true");
 
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
-
         parsed_flags flags = parsed_flags
                 .newBuilder()
                 .addParsedFlag(parsed_flag
@@ -799,8 +774,7 @@
 
         synchronized (mLock) {
             settingsState.loadAconfigDefaultValues(
-                    flags.toByteArray(),
-                    settingsState.getAconfigDefaultValues(), flagInfoDefault);
+                    flags.toByteArray(), settingsState.getAconfigDefaultValues());
             List<String> updates =
                     settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
             assertEquals(1, updates.size());
@@ -866,13 +840,10 @@
                         .setState(Aconfig.flag_state.DISABLED)
                         .setPermission(Aconfig.flag_permission.READ_WRITE))
                 .build();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
 
         synchronized (mLock) {
             settingsState.loadAconfigDefaultValues(
-                    flags.toByteArray(),
-                    settingsState.getAconfigDefaultValues(),
-                    flagInfoDefault);
+                    flags.toByteArray(), settingsState.getAconfigDefaultValues());
             List<String> updates =
                     settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
             assertEquals(3, updates.size());
@@ -1002,12 +973,10 @@
                         .setState(Aconfig.flag_state.DISABLED)
                         .setPermission(Aconfig.flag_permission.READ_WRITE))
                 .build();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
 
         synchronized (lock) {
             Map<String, Map<String, String>> defaults = new HashMap<>();
-            settingsState.loadAconfigDefaultValues(
-                flags.toByteArray(), defaults, flagInfoDefault);
+            settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults);
             Map<String, String> namespaceDefaults = defaults.get("test_namespace");
             assertEquals(1, namespaceDefaults.keySet().size());
             settingsState.addAconfigDefaultValuesFromMap(defaults);
@@ -1022,28 +991,22 @@
             "some_namespace/some_flag", "false") == null);
 
         // server override
-        AconfigdFlagInfo flag = settingsState.getFlagOverrideToSync(
+        FlagOverrideToSync flag = settingsState.getFlagOverrideToSync(
             "test_namespace/com.android.flags.flag1", "false");
         assertTrue(flag != null);
-        assertEquals(flag.getPackageName(), "com.android.flags");
-        assertEquals(flag.getFlagName(), "flag1");
-        assertEquals("false", flag.getBootFlagValue());
-        assertEquals("false", flag.getServerFlagValue());
-        assertFalse(flag.getHasLocalOverride());
-        assertNull(flag.getLocalFlagValue());
-        assertEquals("false", flag.getDefaultFlagValue());
+        assertEquals(flag.packageName, "com.android.flags");
+        assertEquals(flag.flagName, "flag1");
+        assertEquals(flag.flagValue, "false");
+        assertEquals(flag.isLocal, false);
 
         // local override
         flag = settingsState.getFlagOverrideToSync(
             "device_config_overrides/test_namespace:com.android.flags.flag1", "false");
         assertTrue(flag != null);
-        assertEquals(flag.getPackageName(), "com.android.flags");
-        assertEquals(flag.getFlagName(), "flag1");
-        assertEquals("false", flag.getLocalFlagValue());
-        assertEquals("false", flag.getBootFlagValue());
-        assertTrue(flag.getHasLocalOverride());
-        assertNull(flag.getServerFlagValue());
-        assertEquals("false", flag.getDefaultFlagValue());
+        assertEquals(flag.packageName, "com.android.flags");
+        assertEquals(flag.flagName, "flag1");
+        assertEquals(flag.flagValue, "false");
+        assertEquals(flag.isLocal, true);
     }
 
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -1057,25 +1020,18 @@
                 InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey,
                 SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
 
-        Map<String, AconfigdFlagInfo> flags = new HashMap<>();
-        AconfigdFlagInfo flag = AconfigdFlagInfo.newBuilder()
-        .setPackageName("com.android.flags")
-                .setFlagName("flag1")
-                .setBootFlagValue("true").build();
-        flags.put("com.android.flags/flag1", flag);
-
         synchronized (lock) {
             settingsState.insertSettingLocked("aconfigd_marker/bulk_synced",
                     "false", null, false, "aconfig");
 
             // first bulk sync
-            ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags);
+            ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage();
             assertTrue(requests != null);
             String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
             assertEquals("true", value);
 
             // send time should no longer bulk sync
-            requests = settingsState.handleBulkSyncToNewStorage(flags);
+            requests = settingsState.handleBulkSyncToNewStorage();
             assertTrue(requests == null);
             value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
             assertEquals("true", value);
@@ -1091,200 +1047,21 @@
                 InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey,
                 SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
 
-        Map<String, AconfigdFlagInfo> flags = new HashMap<>();
         synchronized (lock) {
             settingsState.insertSettingLocked("aconfigd_marker/bulk_synced",
                     "true", null, false, "aconfig");
 
             // when aconfigd is off, should change the marker to false
-            ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage(flags);
+            ProtoOutputStream requests = settingsState.handleBulkSyncToNewStorage();
             assertTrue(requests == null);
             String value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
             assertEquals("false", value);
 
             // marker started with false value, after call, it should remain false
-            requests = settingsState.handleBulkSyncToNewStorage(flags);
+            requests = settingsState.handleBulkSyncToNewStorage();
             assertTrue(requests == null);
             value = settingsState.getSettingLocked("aconfigd_marker/bulk_synced").getValue();
             assertEquals("false", value);
         }
     }
-
-    @Test
-    public void testGetAllAconfigFlagsFromSettings() throws Exception {
-        final Object lock = new Object();
-        final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
-        os.print(
-                "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
-                        + "<settings version=\"120\">"
-                        + "  <setting id=\"0\" name=\"test_namespace/com.android.flags.flag1\" "
-                            + "value=\"false\" package=\"com.android.flags\" />"
-                        + "  <setting id=\"1\" name=\"device_config_overrides/test_namespace:com.android.flags.flag1\" "
-                            + "value=\"true\" package=\"com.android.flags\" />"
-                        + "  <setting id=\"2\" name=\"device_config_overrides/test_namespace:com.android.flags.flag2\" "
-                            + "value=\"true\" package=\"com.android.flags\" />"
-                        + "  <setting id=\"3\" name=\"test_namespace/com.android.flags.flag3\" "
-                            + "value=\"true\" package=\"com.android.flags\" />"
-                        + "</settings>");
-        os.close();
-
-        int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
-
-        SettingsState settingsState = new SettingsState(
-                InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey,
-                SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
-
-        Map<String, AconfigdFlagInfo> ret;
-        synchronized (lock) {
-            ret = settingsState.getAllAconfigFlagsFromSettings();
-        }
-
-        assertTrue(ret.isEmpty());
-
-        parsed_flags flags =
-                parsed_flags
-                        .newBuilder()
-                        .addParsedFlag(
-                                parsed_flag
-                                        .newBuilder()
-                                        .setPackage("com.android.flags")
-                                        .setName("flag1")
-                                        .setNamespace("test_namespace")
-                                        .setDescription("test flag")
-                                        .addBug("12345678")
-                                        .setState(Aconfig.flag_state.DISABLED)
-                                        .setPermission(Aconfig.flag_permission.READ_WRITE))
-                        .addParsedFlag(
-                                parsed_flag
-                                        .newBuilder()
-                                        .setPackage("com.android.flags")
-                                        .setName("flag2")
-                                        .setNamespace("test_namespace")
-                                        .setDescription("test flag")
-                                        .addBug("12345678")
-                                        .setState(Aconfig.flag_state.DISABLED)
-                                        .setPermission(Aconfig.flag_permission.READ_WRITE))
-                        .addParsedFlag(
-                                parsed_flag
-                                        .newBuilder()
-                                        .setPackage("com.android.flags")
-                                        .setName("flag3")
-                                        .setNamespace("test_namespace")
-                                        .setDescription("test flag")
-                                        .addBug("12345678")
-                                        .setState(Aconfig.flag_state.DISABLED)
-                                        .setPermission(Aconfig.flag_permission.READ_WRITE))
-                        .build();
-
-        Map<String, Map<String, String>> defaults = new HashMap<>();
-        Map<String, AconfigdFlagInfo> flagInfoDefault = new HashMap<>();
-        synchronized (lock) {
-            settingsState.loadAconfigDefaultValues(
-                flags.toByteArray(), defaults, flagInfoDefault);
-            settingsState.addAconfigDefaultValuesFromMap(defaults);
-            ret = settingsState.getAllAconfigFlagsFromSettings();
-        }
-
-        AconfigdFlagInfo expectedFlag1 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag1")
-                        .setServerFlagValue("false")
-                        .setLocalFlagValue("true")
-                        .setDefaultFlagValue("false")
-                        .setBootFlagValue("true")
-                        .setHasServerOverride(true)
-                        .setHasLocalOverride(true)
-                        .setIsReadWrite(false)
-                        .build();
-
-        AconfigdFlagInfo expectedFlag2 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag2")
-                        .setLocalFlagValue("true")
-                        .setDefaultFlagValue("false")
-                        .setBootFlagValue("true")
-                        .setHasLocalOverride(true)
-                        .setHasServerOverride(false)
-                        .setIsReadWrite(false)
-                        .build();
-
-
-        AconfigdFlagInfo expectedFlag3 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag3")
-                        .setServerFlagValue("true")
-                        .setBootFlagValue("true")
-                        .setDefaultFlagValue("false")
-                        .setHasServerOverride(true)
-                        .setIsReadWrite(false)
-                        .build();
-
-        assertEquals(expectedFlag1, ret.get("com.android.flags.flag1"));
-        assertEquals(expectedFlag2, ret.get("com.android.flags.flag2"));
-        assertEquals(expectedFlag3, ret.get("com.android.flags.flag3"));
-    }
-
-    @Test
-    public void testCompareFlagValueInNewStorage() {
-                int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
-        Object lock = new Object();
-        SettingsState settingsState =
-                new SettingsState(
-                        InstrumentationRegistry.getContext(),
-                        lock,
-                        mSettingsFile,
-                        configKey,
-                        SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED,
-                        Looper.getMainLooper());
-
-        AconfigdFlagInfo defaultFlag1 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag1")
-                        .setDefaultFlagValue("false")
-                        .build();
-
-        AconfigdFlagInfo settingFlag1 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag1")
-                        .setServerFlagValue("true")
-                        .setHasServerOverride(true)
-                        .build();
-
-        AconfigdFlagInfo expectedFlag1 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag1")
-                        .setBootFlagValue("true")
-                        .setServerFlagValue("true")
-                        .setDefaultFlagValue("false")
-                        .setHasServerOverride(true)
-                        .build();
-
-        Map<String, AconfigdFlagInfo> settingMap = new HashMap<>();
-        Map<String, AconfigdFlagInfo> aconfigdMap = new HashMap<>();
-        Map<String, AconfigdFlagInfo> defaultMap = new HashMap<>();
-
-        defaultMap.put("com.android.flags.flag1", defaultFlag1);
-        settingMap.put("com.android.flags.flag1", settingFlag1);
-        aconfigdMap.put("com.android.flags.flag1", expectedFlag1);
-
-        int ret = settingsState.compareFlagValueInNewStorage(settingMap, defaultMap, aconfigdMap);
-        assertEquals(0, ret);
-
-        AconfigdFlagInfo defaultFlag2 =
-                AconfigdFlagInfo.newBuilder()
-                        .setPackageName("com.android.flags")
-                        .setFlagName("flag2")
-                        .setDefaultFlagValue("false")
-                        .build();
-        defaultMap.put("com.android.flags.flag2", defaultFlag2);
-
-        ret = settingsState.compareFlagValueInNewStorage(settingMap, defaultMap, aconfigdMap);
-        assertEquals(1, ret);
-    }
 }
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/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..a730206 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
@@ -79,7 +79,10 @@
                 },
             horizontalArrangement = horizontalArrangement,
         ) {
-            Panel(modifier = Modifier.panelSize(), content = content)
+            Panel(
+                modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(),
+                content = content
+            )
         }
     }
 }
@@ -138,6 +141,7 @@
 object OverlayShade {
     object Elements {
         val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker)
+        val Panel = ElementKey("OverlayShadePanel", scenePicker = LowestZIndexScenePicker)
         val PanelBackground =
             ElementKey("OverlayShadePanelBackground", scenePicker = LowestZIndexScenePicker)
     }
@@ -153,6 +157,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/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/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/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/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/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/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 64717fc..1e0adec 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -1665,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/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/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/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..8316b3a 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
@@ -217,7 +217,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 +347,7 @@
         inflateSettingsButton()
         mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
         configurationController.addCallback(configListener)
-        if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (!mediaFlags.isSceneContainerEnabled()) {
             setUpListeners()
         } else {
             val visualStabilityCallback = OnReorderingAllowedListener {
@@ -389,7 +389,7 @@
                 listenForAnyStateToLockscreenTransition(this)
                 listenForLockscreenSettingChanges(this)
 
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) return@repeatOnLifecycle
+                if (!mediaFlags.isSceneContainerEnabled()) return@repeatOnLifecycle
                 listenForMediaItemsChanges(this)
             }
         }
@@ -882,8 +882,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 +1091,7 @@
     }
 
     private fun updatePlayers(recreateMedia: Boolean) {
-        if (mediaFlags.isMediaControlsRefactorEnabled()) {
+        if (mediaFlags.isSceneContainerEnabled()) {
             updateMediaPlayers(recreateMedia)
             return
         }
@@ -1192,7 +1191,7 @@
             currentStartLocation = startLocation
             currentEndLocation = endLocation
             currentTransitionProgress = progress
-            if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+            if (!mediaFlags.isSceneContainerEnabled()) {
                 for (mediaPlayer in MediaPlayerData.players()) {
                     updateViewControllerToState(mediaPlayer.mediaViewController, immediately)
                 }
@@ -1254,7 +1253,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 +1268,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 +1360,7 @@
                         !mediaManager.hasActiveMediaOrRecommendation() &&
                         desiredHostState.showsOnlyActiveMedia
 
-                if (!mediaFlags.isMediaControlsRefactorEnabled()) {
+                if (!mediaFlags.isSceneContainerEnabled()) {
                     for (mediaPlayer in MediaPlayerData.players()) {
                         if (animate) {
                             mediaPlayer.mediaViewController.animatePendingStateChange(
@@ -1401,7 +1400,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 +1543,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..2dc09e5 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();
     }
@@ -564,13 +558,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 +613,10 @@
     }
 
     private void updateIsEnabled() {
+        mUiThreadContext.runWithScissors(this::updateIsEnabledInner);
+    }
+
+    private void updateIsEnabledInner() {
         try {
             Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
 
@@ -661,12 +661,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 +677,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 +773,7 @@
         mUseMLModel = newState;
 
         if (mUseMLModel) {
-            Assert.isMainThread();
+            mUiThreadContext.isCurrentThread();
             if (mMLModelIsLoading) {
                 Log.d(TAG, "Model tried to load while already loading.");
                 return;
@@ -804,12 +804,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 +1292,7 @@
         updateBackAnimationThresholds();
         if (mLightBarControllerProvider.get() != null) {
             mBackAnimation.setStatusBarCustomizer((appearance) -> {
-                mMainExecutor.execute(() ->
+                mUiThreadContext.getExecutor().execute(() ->
                         mLightBarControllerProvider.get()
                                 .customizeStatusBarAppearance(appearance));
             });
@@ -1308,8 +1309,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 +1327,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 +1365,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/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 fb980d9..8c0d122 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -163,6 +163,9 @@
 
     private boolean mIsSmallScreen;
 
+    /** Should the squishiness fraction be updated on the media host. */
+    private boolean mShouldUpdateMediaSquishiness;
+
     private CommandQueue mCommandQueue;
 
     private View mRootView;
@@ -619,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;
@@ -697,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.
@@ -995,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/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/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/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
index 83e3428..a7abb6b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt
@@ -19,6 +19,7 @@
 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
@@ -31,6 +32,12 @@
 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
@@ -106,6 +113,39 @@
         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.
      *
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..e0f64b4 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
@@ -18,8 +18,6 @@
 
 import android.bluetooth.BluetoothAdapter
 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
@@ -81,30 +79,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 = productName.toString(),
+            icon = deviceIconInteractor.loadIcon(type),
+        )
     }
 
     private fun MediaDevice.toAudioOutputDevice(): AudioOutputDevice {
@@ -115,7 +115,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/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/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/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/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
index 5f7c386..20fb701 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
@@ -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,6 +76,8 @@
 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
@@ -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..80ebe56 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
@@ -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/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 a1c5a5f..09d6a1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -255,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;
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/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/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/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 42f168b..77decb6 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);
     }
@@ -1925,6 +1949,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 +2005,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 +2416,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 +2443,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 +2507,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..fe08338 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();
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/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/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/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 2a23b9c..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;
@@ -57,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;
@@ -239,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;
 
@@ -272,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;
@@ -302,6 +312,8 @@
     private final SettingsAdapter mSettings;
     private final AudioPolicyFacade mAudioPolicy;
 
+    private final AudioServerPermissionProvider mPermissionProvider;
+
     private final MusicFxHelper mMusicFxHelper;
 
     /** Debug audio mode */
@@ -632,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
@@ -1009,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
@@ -1093,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}
@@ -1125,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;
@@ -1472,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 =
@@ -1911,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");
 
@@ -3737,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) {
@@ -4530,6 +4562,8 @@
                 + 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:"
@@ -4540,6 +4574,8 @@
                 + scoManagedByAudio());
         pw.println("\tcom.android.media.audio.vgsVssSyncMuteOrder:"
                 + vgsVssSyncMuteOrder());
+        pw.println("\tcom.android.media.audio.absVolumeIndexFix:"
+                + absVolumeIndexFix());
     }
 
     private void dumpAudioMode(PrintWriter pw) {
@@ -4735,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) {
@@ -6184,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);
@@ -8105,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.
@@ -8755,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) {
@@ -8783,6 +8842,11 @@
                     && !isFullyMuted()) {
                 index = 1;
             }
+
+            if (DEBUG_VOL) {
+                Log.d(TAG, "setStreamVolumeIndexAS(" + mStreamType + ", " + index + ", " + device
+                        + ")");
+            }
             mAudioSystem.setStreamVolumeIndexAS(mStreamType, index, device);
         }
 
@@ -8794,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);
         }
 
@@ -8819,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;
                         }
@@ -9820,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);
@@ -11836,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/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/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/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 5843d72..7513c40 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1547,6 +1547,10 @@
                 InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
                         getPackageManagerForUser(mContext, currentUserId),
                         newSettings.getEnabledInputMethodList());
+
+                final var unused = SystemServerInitThreadPool.submit(
+                        AdditionalSubtypeMapRepository::startWriterThread,
+                        "Start AdditionalSubtypeMapRepository's writer thread");
             }
         }
     }
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/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index e00e813..f4b61e7 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.
@@ -362,6 +362,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 +377,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 +397,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 +420,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 +442,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 +458,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 +472,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 +481,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 +494,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 +515,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 +552,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 +587,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 +623,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 +685,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 +737,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 +746,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 +774,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 +808,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 +821,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 +835,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 +864,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 +892,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 +932,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 +1181,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 +1193,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 +1212,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 +1383,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 +1403,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 +1479,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 +1521,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 +1537,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 +1570,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 +1610,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 +1627,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 +1644,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 +1661,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 +1700,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 +1708,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 +1716,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 +1760,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 +1783,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 +2105,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 +2176,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 +2384,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 8611599..47bd4d3 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 || isInTransition()) {
+            if (isNextNotYetVisible || delayRemoval) {
                 // 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 ac2c886..207707e 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -28,7 +28,6 @@
 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;
 
@@ -386,10 +385,6 @@
                     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/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 94a2239..a00b6fc4 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -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) {
@@ -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,12 +2743,10 @@
     }
 
     private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) {
-        final InsetsSourceControl[] controls =
-                win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win);
         // 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.
-        outArray.set(controls, true /* copyControls */);
+        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.
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index d7c49ac..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,17 +3840,14 @@
         if (mRemoved) {
             return;
         }
-        final InsetsStateController stateController =
-                getDisplayContent().getInsetsStateController();
-        final InsetsState insetsState = getCompatInsetsState();
-        mLastReportedActiveControls.set(stateController.getControlsForDispatch(this),
-                false /* copyControls */);
+        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/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/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/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/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)
+    }
+}