Merge "Revert "Add WindowDecorViewHost and WindowDecorViewHostSupplier"" into main
diff --git a/PERFORMANCE_OWNERS b/PERFORMANCE_OWNERS
index 02b0a1e..d5d752f 100644
--- a/PERFORMANCE_OWNERS
+++ b/PERFORMANCE_OWNERS
@@ -7,3 +7,4 @@
 jdduke@google.com
 shombert@google.com
 kevinjeon@google.com
+yforta@google.com
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index 1e9a79b..4fb0601 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -680,6 +680,7 @@
         dest.writeParcelable(mStartIntent, flags);
         dest.writeInt(mLaunchMode);
         dest.writeBoolean(mWasForceStopped);
+        dest.writeLong(mMonoticCreationTimeMs);
     }
 
     /** @hide */
diff --git a/core/java/android/app/BroadcastStickyCache.java b/core/java/android/app/BroadcastStickyCache.java
new file mode 100644
index 0000000..d6f061b
--- /dev/null
+++ b/core/java/android/app/BroadcastStickyCache.java
@@ -0,0 +1,229 @@
+/*
+ * 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.app;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.TetheringManager;
+import android.net.nsd.NsdManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.SystemProperties;
+import android.os.UpdateLock;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.view.WindowManagerPolicyConstants;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+
+/** @hide */
+public class BroadcastStickyCache {
+
+    private static final String[] CACHED_BROADCAST_ACTIONS = {
+            AudioManager.ACTION_HDMI_AUDIO_PLUG,
+            AudioManager.ACTION_HEADSET_PLUG,
+            AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED,
+            AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED,
+            AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+            AudioManager.RINGER_MODE_CHANGED_ACTION,
+            ConnectivityManager.CONNECTIVITY_ACTION,
+            Intent.ACTION_BATTERY_CHANGED,
+            Intent.ACTION_DEVICE_STORAGE_FULL,
+            Intent.ACTION_DEVICE_STORAGE_LOW,
+            Intent.ACTION_SIM_STATE_CHANGED,
+            NsdManager.ACTION_NSD_STATE_CHANGED,
+            TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED,
+            TetheringManager.ACTION_TETHER_STATE_CHANGED,
+            UpdateLock.UPDATE_LOCK_CHANGED,
+            UsbManager.ACTION_USB_STATE,
+            WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED,
+            WifiManager.NETWORK_STATE_CHANGED_ACTION,
+            WifiManager.SUPPLICANT_STATE_CHANGED_ACTION,
+            WifiManager.WIFI_STATE_CHANGED_ACTION,
+            WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
+            WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED,
+            "android.net.conn.INET_CONDITION_ACTION" // ConnectivityManager.INET_CONDITION_ACTION
+    };
+
+    @GuardedBy("sCachedStickyBroadcasts")
+    private static final ArrayList<CachedStickyBroadcast> sCachedStickyBroadcasts =
+            new ArrayList<>();
+
+    @GuardedBy("sCachedPropertyHandles")
+    private static final ArrayMap<String, SystemProperties.Handle> sCachedPropertyHandles =
+            new ArrayMap<>();
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static boolean useCache(@Nullable IntentFilter filter) {
+        if (!shouldCache(filter)) {
+            return false;
+        }
+        synchronized (sCachedStickyBroadcasts) {
+            final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+            if (cachedStickyBroadcast == null) {
+                return false;
+            }
+            final long version = cachedStickyBroadcast.propertyHandle.getLong(-1 /* def */);
+            return version > 0 && cachedStickyBroadcast.version == version;
+        }
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public static void add(@Nullable IntentFilter filter, @Nullable Intent intent) {
+        if (!shouldCache(filter)) {
+            return;
+        }
+        synchronized (sCachedStickyBroadcasts) {
+            CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+            if (cachedStickyBroadcast == null) {
+                final String key = getKey(filter.getAction(0));
+                final SystemProperties.Handle handle = SystemProperties.find(key);
+                final long version = handle == null ? -1 : handle.getLong(-1 /* def */);
+                if (version == -1) {
+                    return;
+                }
+                cachedStickyBroadcast = new CachedStickyBroadcast(filter, handle);
+                sCachedStickyBroadcasts.add(cachedStickyBroadcast);
+                cachedStickyBroadcast.intent = intent;
+                cachedStickyBroadcast.version = version;
+            } else {
+                cachedStickyBroadcast.intent = intent;
+                cachedStickyBroadcast.version = cachedStickyBroadcast.propertyHandle
+                        .getLong(-1 /* def */);
+            }
+        }
+    }
+
+    private static boolean shouldCache(@Nullable IntentFilter filter) {
+        if (!Flags.useStickyBcastCache()) {
+            return false;
+        }
+        if (filter == null || filter.safeCountActions() != 1) {
+            return false;
+        }
+        if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, filter.getAction(0))) {
+            return false;
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    public static String getKey(@NonNull String action) {
+        return "cache_key.system_server.sticky_bcast." + action;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @Nullable
+    public static Intent getIntentUnchecked(@NonNull IntentFilter filter) {
+        synchronized (sCachedStickyBroadcasts) {
+            final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+            return cachedStickyBroadcast.intent;
+        }
+    }
+
+    @GuardedBy("sCachedStickyBroadcasts")
+    @Nullable
+    private static CachedStickyBroadcast getValueUncheckedLocked(@NonNull IntentFilter filter) {
+        for (int i = sCachedStickyBroadcasts.size() - 1; i >= 0; --i) {
+            final CachedStickyBroadcast cachedStickyBroadcast = sCachedStickyBroadcasts.get(i);
+            if (IntentFilter.filterEquals(filter, cachedStickyBroadcast.filter)) {
+                return cachedStickyBroadcast;
+            }
+        }
+        return null;
+    }
+
+    public static void incrementVersion(@NonNull String action) {
+        if (!shouldIncrementVersion(action)) {
+            return;
+        }
+        final String key = getKey(action);
+        synchronized (sCachedPropertyHandles) {
+            SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+            final long version;
+            if (handle == null) {
+                handle = SystemProperties.find(key);
+                if (handle != null) {
+                    sCachedPropertyHandles.put(key, handle);
+                }
+            }
+            version = handle == null ? 0 : handle.getLong(0 /* def */);
+            SystemProperties.set(key, String.valueOf(version + 1));
+            if (handle == null) {
+                sCachedPropertyHandles.put(key, SystemProperties.find(key));
+            }
+        }
+    }
+
+    public static void incrementVersionIfExists(@NonNull String action) {
+        if (!shouldIncrementVersion(action)) {
+            return;
+        }
+        final String key = getKey(action);
+        synchronized (sCachedPropertyHandles) {
+            final SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+            if (handle == null) {
+                return;
+            }
+            final long version = handle.getLong(0 /* def */);
+            SystemProperties.set(key, String.valueOf(version + 1));
+        }
+    }
+
+    private static boolean shouldIncrementVersion(@NonNull String action) {
+        if (!Flags.useStickyBcastCache()) {
+            return false;
+        }
+        if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, action)) {
+            return false;
+        }
+        return true;
+    }
+
+    @VisibleForTesting
+    public static void clearForTest() {
+        synchronized (sCachedStickyBroadcasts) {
+            sCachedStickyBroadcasts.clear();
+        }
+        synchronized (sCachedPropertyHandles) {
+            sCachedPropertyHandles.clear();
+        }
+    }
+
+    private static final class CachedStickyBroadcast {
+        @NonNull public final IntentFilter filter;
+        @Nullable public Intent intent;
+        @IntRange(from = 0) public long version;
+        @NonNull public final SystemProperties.Handle propertyHandle;
+
+        CachedStickyBroadcast(@NonNull IntentFilter filter,
+                @NonNull SystemProperties.Handle propertyHandle) {
+            this.filter = filter;
+            this.propertyHandle = propertyHandle;
+        }
+    }
+}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 90fba29..ecef0db 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1921,10 +1921,19 @@
             }
         }
         try {
-            final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
-                    mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
-                    AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
-                    flags);
+            final Intent intent;
+            if (receiver == null && BroadcastStickyCache.useCache(filter)) {
+                intent = BroadcastStickyCache.getIntentUnchecked(filter);
+            } else {
+                intent = ActivityManager.getService().registerReceiverWithFeature(
+                        mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
+                        AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission,
+                        userId,
+                        flags);
+                if (receiver == null) {
+                    BroadcastStickyCache.add(filter, intent);
+                }
+            }
             if (intent != null) {
                 intent.setExtrasClassLoader(getClassLoader());
                 // TODO: determine at registration time if caller is
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index cd7e40c..d363e19 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -38,6 +38,7 @@
 per-file IGameManager* = file:/GAME_MANAGER_OWNERS
 per-file IGameMode* = file:/GAME_MANAGER_OWNERS
 per-file BackgroundStartPrivileges.java = file:/BAL_OWNERS
+per-file activity_manager.aconfig = file:/ACTIVITY_MANAGER_OWNERS
 
 # ActivityThread
 per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5ed1f4e..637187e 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -177,6 +177,10 @@
         {
             "file_patterns": ["(/|^)AppOpsManager.java"],
             "name": "CtsAppOpsTestCases"
+        },
+        {
+            "file_patterns": ["(/|^)BroadcastStickyCache.java"],
+            "name": "BroadcastUnitTests"
         }
     ]
 }
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 38bd576..56488e7 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -147,3 +147,14 @@
          purpose: PURPOSE_BUGFIX
      }
 }
+
+flag {
+     namespace: "backstage_power"
+     name: "use_sticky_bcast_cache"
+     description: "Use cache for sticky broadcast intents"
+     is_fixed_read_only: true
+     bug: "356148006"
+     metadata {
+         purpose: PURPOSE_BUGFIX
+     }
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index b0791e3..bca5bcc 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -251,17 +251,18 @@
 }
 
 flag {
-    name: "replace_body_sensors_permission_enabled"
-    is_exported: true
-    namespace: "android_health_services"
-    description: "This flag is used to enable replacing permission BODY_SENSORS(and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE(and READ_HEALTH_DATA_IN_BACKGROUND)"
-    bug: "364638912"
-}
-
-flag {
     name: "appop_access_tracking_logging_enabled"
     is_fixed_read_only: true
     namespace: "permissions"
     description: "Enables logging of the AppOp access tracking"
     bug: "365584286"
 }
+
+flag {
+    name: "replace_body_sensor_permission_enabled"
+    is_fixed_read_only: true
+    is_exported: true
+    namespace: "android_health_services"
+    description: "This fixed read-only flag is used to enable replacing permission BODY_SENSORS (and BODY_SENSORS_BACKGROUND) with granular health permission READ_HEART_RATE (and READ_HEALTH_DATA_IN_BACKGROUND)"
+    bug: "364638912"
+}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index c83285a..3599332 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -168,3 +168,13 @@
   description: "Decouple variation settings, weight and style information from Typeface class"
   bug: "361260253"
 }
+
+flag {
+  name: "handwriting_track_disabled"
+  namespace: "text"
+  description: "Handwriting initiator tracks focused view even if handwriting is disabled to fix initiation bug."
+  bug: "361256391"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index ab9bd1f..f132963 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static com.android.text.flags.Flags.handwritingCursorPosition;
+import static com.android.text.flags.Flags.handwritingTrackDisabled;
 import static com.android.text.flags.Flags.handwritingUnsupportedMessage;
 
 import android.annotation.FlaggedApi;
@@ -352,7 +353,7 @@
 
         final View focusedView = getFocusedView();
 
-        if (!view.isAutoHandwritingEnabled()) {
+        if (!handwritingTrackDisabled() && !view.isAutoHandwritingEnabled()) {
             clearFocusedView(focusedView);
             return;
         }
@@ -363,7 +364,8 @@
         updateFocusedView(view);
 
         if (mState != null && mState.mPendingFocusedView != null
-                && mState.mPendingFocusedView.get() == view) {
+                && mState.mPendingFocusedView.get() == view
+                && (!handwritingTrackDisabled() || view.isAutoHandwritingEnabled())) {
             startHandwriting(view);
         }
     }
@@ -416,7 +418,7 @@
      */
     @VisibleForTesting
     public boolean updateFocusedView(@NonNull View view) {
-        if (!view.shouldInitiateHandwriting()) {
+        if (!handwritingTrackDisabled() && !view.shouldInitiateHandwriting()) {
             mFocusedView = null;
             return false;
         }
@@ -424,8 +426,10 @@
         final View focusedView = getFocusedView();
         if (focusedView != view) {
             mFocusedView = new WeakReference<>(view);
-            // A new view just gain focus. By default, we should show hover icon for it.
-            mShowHoverIconForConnectedView = true;
+            if (!handwritingTrackDisabled() || view.shouldInitiateHandwriting()) {
+                // A new view just gain focus. By default, we should show hover icon for it.
+                mShowHoverIconForConnectedView = true;
+            }
         }
 
         return true;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index d46e1f2..1cad81b 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -130,7 +130,6 @@
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.text.flags.Flags.disableHandwritingInitiatorForIme;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
-import static com.android.window.flags.Flags.insetsControlChangedItem;
 import static com.android.window.flags.Flags.insetsControlSeq;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
 import static com.android.window.flags.Flags.systemUiImmersiveConfirmationDialog;
@@ -11519,12 +11518,8 @@
         public void insetsControlChanged(InsetsState insetsState,
                 InsetsSourceControl.Array activeControls) {
             final boolean isFromInsetsControlChangeItem;
-            if (insetsControlChangedItem()) {
-                isFromInsetsControlChangeItem = mIsFromTransactionItem;
-                mIsFromTransactionItem = false;
-            } else {
-                isFromInsetsControlChangeItem = false;
-            }
+            isFromInsetsControlChangeItem = mIsFromTransactionItem;
+            mIsFromTransactionItem = false;
             final ViewRootImpl viewAncestor = mViewAncestor.get();
             if (viewAncestor == null) {
                 if (isFromInsetsControlChangeItem) {
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 9ae3fc1..11d4db3 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -68,16 +68,6 @@
 
 flag {
     namespace: "windowing_sdk"
-    name: "insets_control_changed_item"
-    description: "Pass insetsControlChanged through ClientTransaction to fix the racing"
-    bug: "339380439"
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
-}
-
-flag {
-    namespace: "windowing_sdk"
     name: "insets_control_seq"
     description: "Add seqId to InsetsControls to ensure the stale update is ignored"
     bug: "339380439"
diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java
index e65b4b6..c0a7383 100644
--- a/core/java/com/android/internal/widget/PointerLocationView.java
+++ b/core/java/com/android/internal/widget/PointerLocationView.java
@@ -16,14 +16,19 @@
 
 package com.android.internal.widget;
 
+import static java.lang.Float.NaN;
+
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.Configuration;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Insets;
 import android.graphics.Paint;
 import android.graphics.Paint.FontMetricsInt;
 import android.graphics.Path;
+import android.graphics.PorterDuff;
 import android.graphics.RectF;
 import android.graphics.Region;
 import android.hardware.input.InputManager;
@@ -65,11 +70,14 @@
     private static final PointerState EMPTY_POINTER_STATE = new PointerState();
 
     public static class PointerState {
-        // Trace of previous points.
-        private float[] mTraceX = new float[32];
-        private float[] mTraceY = new float[32];
-        private boolean[] mTraceCurrent = new boolean[32];
-        private int mTraceCount;
+        private float mCurrentX = NaN;
+        private float mCurrentY = NaN;
+        private float mPreviousX = NaN;
+        private float mPreviousY = NaN;
+        private float mFirstX = NaN;
+        private float mFirstY = NaN;
+        private boolean mPreviousPointIsHistorical;
+        private boolean mCurrentPointIsHistorical;
 
         // True if the pointer is down.
         @UnsupportedAppUsage
@@ -96,31 +104,20 @@
         public PointerState() {
         }
 
-        public void clearTrace() {
-            mTraceCount = 0;
-        }
-
-        public void addTrace(float x, float y, boolean current) {
-            int traceCapacity = mTraceX.length;
-            if (mTraceCount == traceCapacity) {
-                traceCapacity *= 2;
-                float[] newTraceX = new float[traceCapacity];
-                System.arraycopy(mTraceX, 0, newTraceX, 0, mTraceCount);
-                mTraceX = newTraceX;
-
-                float[] newTraceY = new float[traceCapacity];
-                System.arraycopy(mTraceY, 0, newTraceY, 0, mTraceCount);
-                mTraceY = newTraceY;
-
-                boolean[] newTraceCurrent = new boolean[traceCapacity];
-                System.arraycopy(mTraceCurrent, 0, newTraceCurrent, 0, mTraceCount);
-                mTraceCurrent= newTraceCurrent;
+        public void addTrace(float x, float y, boolean isHistorical) {
+            if (Float.isNaN(mFirstX)) {
+                mFirstX = x;
+            }
+            if (Float.isNaN(mFirstY)) {
+                mFirstY = y;
             }
 
-            mTraceX[mTraceCount] = x;
-            mTraceY[mTraceCount] = y;
-            mTraceCurrent[mTraceCount] = current;
-            mTraceCount += 1;
+            mPreviousX = mCurrentX;
+            mPreviousY = mCurrentY;
+            mCurrentX = x;
+            mCurrentY = y;
+            mPreviousPointIsHistorical = mCurrentPointIsHistorical;
+            mCurrentPointIsHistorical = isHistorical;
         }
     }
 
@@ -149,6 +146,12 @@
     private final SparseArray<PointerState> mPointers = new SparseArray<PointerState>();
     private final PointerCoords mTempCoords = new PointerCoords();
 
+    // Draw the trace of all pointers in the current gesture in a separate layer
+    // that is not cleared on every frame so that we don't have to re-draw the
+    // entire trace on each frame.
+    private final Bitmap mTraceBitmap;
+    private final Canvas mTraceCanvas;
+
     private final Region mSystemGestureExclusion = new Region();
     private final Region mSystemGestureExclusionRejected = new Region();
     private final Path mSystemGestureExclusionPath = new Path();
@@ -197,6 +200,10 @@
         mPathPaint.setARGB(255, 0, 96, 255);
         mPathPaint.setStyle(Paint.Style.STROKE);
 
+        mTraceBitmap = Bitmap.createBitmap(getResources().getDisplayMetrics().widthPixels,
+                getResources().getDisplayMetrics().heightPixels, Bitmap.Config.ARGB_8888);
+        mTraceCanvas = new Canvas(mTraceBitmap);
+
         configureDensityDependentFactors();
 
         mSystemGestureExclusionPaint = new Paint();
@@ -256,7 +263,7 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         mTextPaint.getFontMetricsInt(mTextMetrics);
-        mHeaderBottom = mHeaderPaddingTop-mTextMetrics.ascent+mTextMetrics.descent+2;
+        mHeaderBottom = mHeaderPaddingTop - mTextMetrics.ascent + mTextMetrics.descent + 2;
         if (false) {
             Log.i("foo", "Metrics: ascent=" + mTextMetrics.ascent
                     + " descent=" + mTextMetrics.descent
@@ -269,6 +276,7 @@
     // Draw an oval.  When angle is 0 radians, orients the major axis vertically,
     // angles less than or greater than 0 radians rotate the major axis left or right.
     private RectF mReusableOvalRect = new RectF();
+
     private void drawOval(Canvas canvas, float x, float y, float major, float minor,
             float angle, Paint paint) {
         canvas.save(Canvas.MATRIX_SAVE_FLAG);
@@ -285,6 +293,8 @@
     protected void onDraw(Canvas canvas) {
         final int NP = mPointers.size();
 
+        canvas.drawBitmap(mTraceBitmap, 0, 0, null);
+
         if (!mSystemGestureExclusion.isEmpty()) {
             mSystemGestureExclusionPath.reset();
             mSystemGestureExclusion.getBoundaryPath(mSystemGestureExclusionPath);
@@ -303,32 +313,9 @@
         // Pointer trace.
         for (int p = 0; p < NP; p++) {
             final PointerState ps = mPointers.valueAt(p);
+            float lastX = ps.mCurrentX, lastY = ps.mCurrentY;
 
-            // Draw path.
-            final int N = ps.mTraceCount;
-            float lastX = 0, lastY = 0;
-            boolean haveLast = false;
-            boolean drawn = false;
-            mPaint.setARGB(255, 128, 255, 255);
-            for (int i=0; i < N; i++) {
-                float x = ps.mTraceX[i];
-                float y = ps.mTraceY[i];
-                if (Float.isNaN(x) || Float.isNaN(y)) {
-                    haveLast = false;
-                    continue;
-                }
-                if (haveLast) {
-                    canvas.drawLine(lastX, lastY, x, y, mPathPaint);
-                    final Paint paint = ps.mTraceCurrent[i - 1] ? mCurrentPointPaint : mPaint;
-                    canvas.drawPoint(lastX, lastY, paint);
-                    drawn = true;
-                }
-                lastX = x;
-                lastY = y;
-                haveLast = true;
-            }
-
-            if (drawn) {
+            if (!Float.isNaN(lastX) && !Float.isNaN(lastY)) {
                 // Draw velocity vector.
                 mPaint.setARGB(255, 255, 64, 128);
                 float xVel = ps.mXVelocity * (1000 / 60);
@@ -353,7 +340,7 @@
                         Math.max(getHeight(), getWidth()), mTargetPaint);
 
                 // Draw current point.
-                int pressureLevel = (int)(ps.mCoords.pressure * 255);
+                int pressureLevel = (int) (ps.mCoords.pressure * 255);
                 mPaint.setARGB(255, pressureLevel, 255, 255 - pressureLevel);
                 canvas.drawPoint(ps.mCoords.x, ps.mCoords.y, mPaint);
 
@@ -424,8 +411,7 @@
                 .append(" / ").append(mMaxNumPointers)
                 .toString(), 1, base, mTextPaint);
 
-        final int count = ps.mTraceCount;
-        if ((mCurDown && ps.mCurDown) || count == 0) {
+        if ((mCurDown && ps.mCurDown) || Float.isNaN(ps.mCurrentX)) {
             canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
                     mTextBackgroundPaint);
             canvas.drawText(mText.clear()
@@ -437,8 +423,8 @@
                     .append("Y: ").append(ps.mCoords.y, 1)
                     .toString(), 1 + itemW * 2, base, mTextPaint);
         } else {
-            float dx = ps.mTraceX[count - 1] - ps.mTraceX[0];
-            float dy = ps.mTraceY[count - 1] - ps.mTraceY[0];
+            float dx = ps.mCurrentX - ps.mFirstX;
+            float dy = ps.mCurrentY - ps.mFirstY;
             canvas.drawRect(itemW, mHeaderPaddingTop, (itemW * 2) - 1, bottom,
                     Math.abs(dx) < mVC.getScaledTouchSlop()
                             ? mTextBackgroundPaint : mTextLevelPaint);
@@ -565,9 +551,9 @@
                 .append(" TouchMinor=").append(coords.touchMinor, 3)
                 .append(" ToolMajor=").append(coords.toolMajor, 3)
                 .append(" ToolMinor=").append(coords.toolMinor, 3)
-                .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1)
+                .append(" Orientation=").append((float) (coords.orientation * 180 / Math.PI), 1)
                 .append("deg")
-                .append(" Tilt=").append((float)(
+                .append(" Tilt=").append((float) (
                         coords.getAxisValue(MotionEvent.AXIS_TILT) * 180 / Math.PI), 1)
                 .append("deg")
                 .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1)
@@ -598,6 +584,7 @@
                 mCurNumPointers = 0;
                 mMaxNumPointers = 0;
                 mVelocity.clear();
+                mTraceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                 if (mAltVelocity != null) {
                     mAltVelocity.clear();
                 }
@@ -646,7 +633,8 @@
                     logCoords("Pointer", action, i, coords, id, event);
                 }
                 if (ps != null) {
-                    ps.addTrace(coords.x, coords.y, false);
+                    ps.addTrace(coords.x, coords.y, true);
+                    updateDrawTrace(ps);
                 }
             }
         }
@@ -659,7 +647,8 @@
                 logCoords("Pointer", action, i, coords, id, event);
             }
             if (ps != null) {
-                ps.addTrace(coords.x, coords.y, true);
+                ps.addTrace(coords.x, coords.y, false);
+                updateDrawTrace(ps);
                 ps.mXVelocity = mVelocity.getXVelocity(id);
                 ps.mYVelocity = mVelocity.getYVelocity(id);
                 if (mAltVelocity != null) {
@@ -702,13 +691,26 @@
                 if (mActivePointerId == id) {
                     mActivePointerId = event.getPointerId(index == 0 ? 1 : 0);
                 }
-                ps.addTrace(Float.NaN, Float.NaN, false);
+                ps.addTrace(Float.NaN, Float.NaN, true);
             }
         }
 
         invalidate();
     }
 
+    private void updateDrawTrace(PointerState ps) {
+        mPaint.setARGB(255, 128, 255, 255);
+        float x = ps.mCurrentX;
+        float y = ps.mCurrentY;
+        float lastX = ps.mPreviousX;
+        float lastY = ps.mPreviousY;
+        if (!Float.isNaN(x) && !Float.isNaN(y) && !Float.isNaN(lastX) && !Float.isNaN(lastY)) {
+            mTraceCanvas.drawLine(lastX, lastY, x, y, mPathPaint);
+            Paint paint = ps.mPreviousPointIsHistorical ? mPaint : mCurrentPointPaint;
+            mTraceCanvas.drawPoint(lastX, lastY, paint);
+        }
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         onPointerEvent(event);
@@ -767,7 +769,7 @@
                 return true;
             default:
                 return KeyEvent.isGamepadButton(keyCode)
-                    || KeyEvent.isModifierKey(keyCode);
+                        || KeyEvent.isModifierKey(keyCode);
         }
     }
 
@@ -887,7 +889,7 @@
         public FasterStringBuilder append(int value, int zeroPadWidth) {
             final boolean negative = value < 0;
             if (negative) {
-                value = - value;
+                value = -value;
                 if (value < 0) {
                     append("-2147483648");
                     return this;
@@ -973,26 +975,27 @@
 
     private ISystemGestureExclusionListener mSystemGestureExclusionListener =
             new ISystemGestureExclusionListener.Stub() {
-        @Override
-        public void onSystemGestureExclusionChanged(int displayId, Region systemGestureExclusion,
-                Region systemGestureExclusionUnrestricted) {
-            Region exclusion = Region.obtain(systemGestureExclusion);
-            Region rejected = Region.obtain();
-            if (systemGestureExclusionUnrestricted != null) {
-                rejected.set(systemGestureExclusionUnrestricted);
-                rejected.op(exclusion, Region.Op.DIFFERENCE);
-            }
-            Handler handler = getHandler();
-            if (handler != null) {
-                handler.post(() -> {
-                    mSystemGestureExclusion.set(exclusion);
-                    mSystemGestureExclusionRejected.set(rejected);
-                    exclusion.recycle();
-                    invalidate();
-                });
-            }
-        }
-    };
+                @Override
+                public void onSystemGestureExclusionChanged(int displayId,
+                        Region systemGestureExclusion,
+                        Region systemGestureExclusionUnrestricted) {
+                    Region exclusion = Region.obtain(systemGestureExclusion);
+                    Region rejected = Region.obtain();
+                    if (systemGestureExclusionUnrestricted != null) {
+                        rejected.set(systemGestureExclusionUnrestricted);
+                        rejected.op(exclusion, Region.Op.DIFFERENCE);
+                    }
+                    Handler handler = getHandler();
+                    if (handler != null) {
+                        handler.post(() -> {
+                            mSystemGestureExclusion.set(exclusion);
+                            mSystemGestureExclusionRejected.set(rejected);
+                            exclusion.recycle();
+                            invalidate();
+                        });
+                    }
+                }
+            };
 
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index fe3d4f6..07efad8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4380,7 +4380,7 @@
              modes dimensions {@link config_minPercentageMultiWindowSupportWidth} the device
              supports to determine if the activity can be shown in multi windowing modes.
     -->
-    <integer name="config_respectsActivityMinWidthHeightMultiWindow">0</integer>
+    <integer name="config_respectsActivityMinWidthHeightMultiWindow">-1</integer>
 
     <!-- This value is only used when the device checks activity min height to determine if it
          can be shown in multi windowing modes.
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 69437b4..9854030 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -287,6 +287,11 @@
     <string name="config_satellite_demo_mode_sos_intent_action" translatable="false"></string>
     <java-symbol type="string" name="config_satellite_demo_mode_sos_intent_action" />
 
+    <!-- The action of the intent that hidden menu sends to the app to launch esp loopback test mode
+     for sos emergency messaging via satellite. -->
+    <string name="config_satellite_test_with_esp_replies_intent_action" translatable="false"></string>
+    <java-symbol type="string" name="config_satellite_test_with_esp_replies_intent_action" />
+
     <!-- Whether outgoing satellite datagrams should be sent to modem in demo mode. When satellite
          is enabled for demo mode, if this config is enabled, outgoing datagrams will be sent to
          modem; otherwise, success results will be returned. If demo mode is disabled, outgoing
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
index 991cdcf..c7b4c65 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
index 991cdcf..c7b4c65 100644
--- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
index c0ff192..1d1cdfa 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
@@ -28,6 +28,8 @@
         android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height"
         android:paddingVertical="16dp"
         android:paddingHorizontal="10dp"
+        android:screenReaderFocusable="true"
+        android:importantForAccessibility="yes"
         android:contentDescription="@string/handle_text"
         android:src="@drawable/decor_handle_dark"
         tools:tint="@color/desktop_mode_caption_handle_bar_dark"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 7dcb3c2..3dbf754 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -31,14 +31,16 @@
         android:orientation="horizontal"
         android:clickable="true"
         android:focusable="true"
+        android:contentDescription="@string/desktop_mode_app_header_chip_text"
         android:layout_marginStart="12dp">
         <ImageView
             android:id="@+id/application_icon"
             android:layout_width="@dimen/desktop_mode_caption_icon_radius"
             android:layout_height="@dimen/desktop_mode_caption_icon_radius"
             android:layout_gravity="center_vertical"
-            android:contentDescription="@string/app_icon_text"
             android:layout_marginStart="6dp"
+            android:clickable="false"
+            android:focusable="false"
             android:scaleType="centerCrop"/>
 
         <TextView
@@ -53,18 +55,22 @@
             android:layout_gravity="center_vertical"
             android:layout_weight="1"
             android:layout_marginStart="8dp"
+            android:clickable="false"
+            android:focusable="false"
             tools:text="Gmail"/>
 
         <ImageButton
             android:id="@+id/expand_menu_button"
             android:layout_width="16dp"
             android:layout_height="16dp"
-            android:contentDescription="@string/expand_menu_text"
             android:src="@drawable/ic_baseline_expand_more_24"
             android:background="@null"
             android:scaleType="fitCenter"
             android:clickable="false"
             android:focusable="false"
+            android:screenReaderFocusable="false"
+            android:importantForAccessibility="no"
+            android:contentDescription="@null"
             android:layout_marginHorizontal="8dp"
             android:layout_gravity="center_vertical"/>
 
@@ -90,6 +96,7 @@
 
     <com.android.wm.shell.windowdecor.MaximizeButtonView
         android:id="@+id/maximize_button_view"
+        android:importantForAccessibility="no"
         android:layout_width="44dp"
         android:layout_height="40dp"
         android:layout_gravity="end"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index 64f71c7..6913e54 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -43,13 +43,15 @@
             android:layout_height="@dimen/desktop_mode_caption_icon_radius"
             android:layout_marginStart="12dp"
             android:layout_marginEnd="12dp"
-            android:contentDescription="@string/app_icon_text"/>
+            android:contentDescription="@string/app_icon_text"
+            android:importantForAccessibility="no"/>
 
         <TextView
             android:id="@+id/application_name"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             tools:text="Gmail"
+            android:importantForAccessibility="no"
             android:textColor="?androidprv:attr/materialColorOnSurface"
             android:textSize="14sp"
             android:textFontWeight="500"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 5fe3f2a..35ef239 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -41,6 +41,8 @@
                 android:id="@+id/maximize_menu_maximize_button"
                 style="?android:attr/buttonBarButtonStyle"
                 android:stateListAnimator="@null"
+                android:importantForAccessibility="yes"
+                android:contentDescription="@string/desktop_mode_maximize_menu_maximize_button_text"
                 android:layout_marginRight="8dp"
                 android:layout_marginBottom="4dp"
                 android:alpha="0"/>
@@ -53,6 +55,7 @@
                 android:layout_marginBottom="76dp"
                 android:gravity="center"
                 android:fontFamily="google-sans-text"
+                android:importantForAccessibility="no"
                 android:text="@string/desktop_mode_maximize_menu_maximize_text"
                 android:textColor="?androidprv:attr/materialColorOnSurface"
                 android:alpha="0"/>
@@ -78,6 +81,8 @@
                     android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
                     android:layout_marginRight="4dp"
                     android:background="@drawable/desktop_mode_maximize_menu_button_background"
+                    android:importantForAccessibility="yes"
+                    android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text"
                     android:stateListAnimator="@null"/>
 
                 <Button
@@ -86,6 +91,8 @@
                     android:layout_width="41dp"
                     android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
                     android:background="@drawable/desktop_mode_maximize_menu_button_background"
+                    android:importantForAccessibility="yes"
+                    android:contentDescription="@string/desktop_mode_maximize_menu_snap_right_button_text"
                     android:stateListAnimator="@null"/>
             </LinearLayout>
             <TextView
@@ -96,6 +103,7 @@
                 android:layout_marginBottom="76dp"
                 android:layout_gravity="center"
                 android:gravity="center"
+                android:importantForAccessibility="no"
                 android:fontFamily="google-sans-text"
                 android:text="@string/desktop_mode_maximize_menu_snap_text"
                 android:textColor="?androidprv:attr/materialColorOnSurface"
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index cf1b894..b734d2d 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -19,7 +19,8 @@
 
     <FrameLayout
         android:layout_width="44dp"
-        android:layout_height="40dp">
+        android:layout_height="40dp"
+        android:importantForAccessibility="noHideDescendants">
         <ProgressBar
             android:id="@+id/progress_bar"
             style="?android:attr/progressBarStyleHorizontal"
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index a6da421..bda5686 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -300,12 +300,19 @@
     <string name="close_text">Close</string>
     <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
     <string name="collapse_menu_text">Close Menu</string>
-    <!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] -->
-    <string name="expand_menu_text">Open Menu</string>
+    <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] -->
+    <string name="desktop_mode_app_header_chip_text">Open Menu</string>
     <!-- Maximize menu maximize button string. -->
     <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
     <!-- Maximize menu snap buttons string. -->
     <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
     <!-- Snap resizing non-resizable string. -->
     <string name="desktop_mode_non_resizable_snap_text">This app can\'t be resized</string>
+    <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] -->
+    <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string>
+    <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] -->
+    <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string>
+    <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] -->
+    <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string>
+
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 3a2820ee..f40e0ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -826,26 +826,24 @@
             @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
             @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
             @Nullable Rect clipRect, boolean isActivity) {
-        final DefaultAnimationAdapter adapter = new DefaultAnimationAdapter(anim, leash,
-                position, clipRect, cornerRadius, isActivity);
-        buildSurfaceAnimation(animations, anim, finishCallback, pool, mainExecutor, adapter);
-    }
-
-    /** Builds an animator for the surface and adds it to the `animations` list. */
-    static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
-            @NonNull Animation anim, @NonNull Runnable finishCallback,
-            @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor,
-            @NonNull AnimationAdapter updateListener) {
         final SurfaceControl.Transaction transaction = pool.acquire();
-        updateListener.setTransaction(transaction);
         final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+        final Transformation transformation = new Transformation();
+        final float[] matrix = new float[9];
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
+        final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
+            final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
+
+            applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
+                    position, cornerRadius, clipRect, isActivity);
+        };
         va.addUpdateListener(updateListener);
 
         final Runnable finisher = () -> {
-            updateListener.onAnimationUpdate(va);
+            applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
+                    position, cornerRadius, clipRect, isActivity);
 
             pool.release(transaction);
             mainExecutor.execute(() -> {
@@ -1009,88 +1007,37 @@
                 || animType == ANIM_FROM_STYLE;
     }
 
-    /** The animation adapter for buildSurfaceAnimation. */
-    abstract static class AnimationAdapter implements ValueAnimator.AnimatorUpdateListener {
-        @NonNull final SurfaceControl mLeash;
-        @NonNull SurfaceControl.Transaction mTransaction;
-        private Choreographer mChoreographer;
+    private static void applyTransformation(long time, SurfaceControl.Transaction t,
+            SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix,
+            Point position, float cornerRadius, @Nullable Rect immutableClipRect,
+            boolean isActivity) {
+        tmpTransformation.clear();
+        anim.getTransformation(time, tmpTransformation);
+        if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
+                && anim.getExtensionEdges() != 0x0 && isActivity) {
+            t.setEdgeExtensionEffect(leash, anim.getExtensionEdges());
+        }
+        if (position != null) {
+            tmpTransformation.getMatrix().postTranslate(position.x, position.y);
+        }
+        t.setMatrix(leash, tmpTransformation.getMatrix(), matrix);
+        t.setAlpha(leash, tmpTransformation.getAlpha());
 
-        AnimationAdapter(@NonNull SurfaceControl leash) {
-            mLeash = leash;
+        final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect);
+        Insets extensionInsets = Insets.min(tmpTransformation.getInsets(), Insets.NONE);
+        if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) {
+            // Clip out any overflowing edge extension
+            clipRect.inset(extensionInsets);
+            t.setCrop(leash, clipRect);
         }
 
-        void setTransaction(@NonNull SurfaceControl.Transaction transaction) {
-            mTransaction = transaction;
+        if (anim.hasRoundedCorners() && cornerRadius > 0 && clipRect != null) {
+            // We can only apply rounded corner if a crop is set
+            t.setCrop(leash, clipRect);
+            t.setCornerRadius(leash, cornerRadius);
         }
 
-        @Override
-        public void onAnimationUpdate(@NonNull ValueAnimator animator) {
-            applyTransformation(animator);
-            if (mChoreographer == null) {
-                mChoreographer = Choreographer.getInstance();
-            }
-            mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
-            mTransaction.apply();
-        }
-
-        abstract void applyTransformation(@NonNull ValueAnimator animator);
-    }
-
-    private static class DefaultAnimationAdapter extends AnimationAdapter {
-        final Transformation mTransformation = new Transformation();
-        final float[] mMatrix = new float[9];
-        @NonNull final Animation mAnim;
-        @Nullable final Point mPosition;
-        @Nullable final Rect mClipRect;
-        final float mCornerRadius;
-        final boolean mIsActivity;
-
-        DefaultAnimationAdapter(@NonNull Animation anim, @NonNull SurfaceControl leash,
-                @Nullable Point position, @Nullable Rect clipRect, float cornerRadius,
-                boolean isActivity) {
-            super(leash);
-            mAnim = anim;
-            mPosition = (position != null && (position.x != 0 || position.y != 0))
-                    ? position : null;
-            mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null;
-            mCornerRadius = cornerRadius;
-            mIsActivity = isActivity;
-        }
-
-        @Override
-        void applyTransformation(@NonNull ValueAnimator animator) {
-            final long currentPlayTime = Math.min(animator.getDuration(),
-                    animator.getCurrentPlayTime());
-            final Transformation transformation = mTransformation;
-            final SurfaceControl.Transaction t = mTransaction;
-            final SurfaceControl leash = mLeash;
-            transformation.clear();
-            mAnim.getTransformation(currentPlayTime, transformation);
-            if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
-                    && mIsActivity && mAnim.getExtensionEdges() != 0) {
-                t.setEdgeExtensionEffect(leash, mAnim.getExtensionEdges());
-            }
-            if (mPosition != null) {
-                transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
-            }
-            t.setMatrix(leash, transformation.getMatrix(), mMatrix);
-            t.setAlpha(leash, transformation.getAlpha());
-
-            if (mClipRect != null) {
-                Rect clipRect = mClipRect;
-                final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
-                if (!extensionInsets.equals(Insets.NONE)) {
-                    // Clip out any overflowing edge extension.
-                    clipRect = new Rect(mClipRect);
-                    clipRect.inset(extensionInsets);
-                    t.setCrop(leash, clipRect);
-                }
-                if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) {
-                    // Rounded corner can only be applied if a crop is set.
-                    t.setCrop(leash, clipRect);
-                    t.setCornerRadius(leash, mCornerRadius);
-                }
-            }
-        }
+        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+        t.apply();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index dff7dee..8a53f5b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -678,7 +678,7 @@
                 || !Flags.enableHandleInputFix()) {
             return;
         }
-        ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer();
+        asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer();
     }
 
     private WindowDecorationViewHolder createViewHolder() {
@@ -715,6 +715,22 @@
         return viewHolder instanceof AppHandleViewHolder;
     }
 
+    @Nullable
+    private AppHandleViewHolder asAppHandle(WindowDecorationViewHolder viewHolder) {
+        if (viewHolder instanceof AppHandleViewHolder) {
+            return (AppHandleViewHolder) viewHolder;
+        }
+        return null;
+    }
+
+    @Nullable
+    private AppHeaderViewHolder asAppHeader(WindowDecorationViewHolder viewHolder) {
+        if (viewHolder instanceof AppHeaderViewHolder) {
+            return (AppHeaderViewHolder) viewHolder;
+        }
+        return null;
+    }
+
     @VisibleForTesting
     static void updateRelayoutParams(
             RelayoutParams relayoutParams,
@@ -1089,7 +1105,15 @@
      */
     void closeMaximizeMenu() {
         if (!isMaximizeMenuActive()) return;
-        mMaximizeMenu.close();
+        mMaximizeMenu.close(() -> {
+            // Request the accessibility service to refocus on the maximize button after closing
+            // the menu.
+            final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder);
+            if (appHeader != null) {
+                appHeader.requestAccessibilityFocus();
+            }
+            return Unit.INSTANCE;
+        });
         mMaximizeMenu = null;
     }
 
@@ -1472,7 +1496,7 @@
 
     void setAnimatingTaskResizeOrReposition(boolean animatingTaskResizeOrReposition) {
         if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return;
-        ((AppHeaderViewHolder) mWindowDecorViewHolder)
+        asAppHeader(mWindowDecorViewHolder)
                 .setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition);
     }
 
@@ -1480,16 +1504,14 @@
      * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button.
      */
     void onMaximizeButtonHoverExit() {
-        ((AppHeaderViewHolder) mWindowDecorViewHolder)
-                .onMaximizeWindowHoverExit();
+        asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverExit();
     }
 
     /**
      * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button.
      */
     void onMaximizeButtonHoverEnter() {
-        ((AppHeaderViewHolder) mWindowDecorViewHolder)
-                .onMaximizeWindowHoverEnter();
+        asAppHeader(mWindowDecorViewHolder).onMaximizeWindowHoverEnter();
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 9590ccd..0c475f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -26,6 +26,7 @@
 import android.view.View.TRANSLATION_Y
 import android.view.View.TRANSLATION_Z
 import android.view.ViewGroup
+import android.view.accessibility.AccessibilityEvent
 import android.widget.Button
 import androidx.core.animation.doOnEnd
 import androidx.core.view.children
@@ -83,7 +84,12 @@
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
         animateOpenInBrowserPill()
-        runAnimations()
+        runAnimations {
+            appInfoPill.post {
+                appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
+                    AccessibilityEvent.TYPE_VIEW_FOCUSED)
+            }
+        }
     }
 
     /**
@@ -98,7 +104,12 @@
         animateWindowingPillOpen()
         animateMoreActionsPillOpen()
         animateOpenInBrowserPill()
-        runAnimations()
+        runAnimations {
+            appInfoPill.post {
+                appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent(
+                    AccessibilityEvent.TYPE_VIEW_FOCUSED)
+            }
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 9c73e4a..0cb219a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -51,6 +51,7 @@
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
+import android.view.accessibility.AccessibilityEvent
 import android.widget.Button
 import android.widget.TextView
 import android.window.TaskConstants
@@ -116,19 +117,24 @@
             onHoverListener = onHoverListener,
             onOutsideTouchListener = onOutsideTouchListener
         )
-        maximizeMenuView?.animateOpenMenu()
+        maximizeMenuView?.let { view ->
+            view.animateOpenMenu(onEnd = {
+                view.requestAccessibilityFocus()
+            })
+        }
     }
 
     /** Closes the maximize window and releases its view. */
-    fun close() {
+    fun close(onEnd: () -> Unit) {
         val view = maximizeMenuView
         val menu = maximizeMenu
         if (view == null) {
             menu?.releaseView()
         } else {
-            view.animateCloseMenu {
+            view.animateCloseMenu(onEnd = {
                 menu?.releaseView()
-            }
+                onEnd.invoke()
+            })
         }
         maximizeMenu = null
         maximizeMenuView = null
@@ -351,7 +357,7 @@
         }
 
         /** Animate the opening of the menu */
-        fun animateOpenMenu() {
+        fun animateOpenMenu(onEnd: () -> Unit) {
             maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
             maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
             menuAnimatorSet = AnimatorSet()
@@ -419,6 +425,7 @@
                 onEnd = {
                     maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
                     maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+                    onEnd.invoke()
                 }
             )
             menuAnimatorSet?.start()
@@ -499,6 +506,14 @@
             menuAnimatorSet?.start()
         }
 
+        /** Request that the accessibility service focus on the menu. */
+        fun requestAccessibilityFocus() {
+            // Focus the first button in the menu by default.
+            maximizeButton.post {
+                maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+            }
+        }
+
         /** Cancel the menu animation. */
         private fun cancelAnimation() {
             menuAnimatorSet?.cancel()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index af6a819..e996165 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -30,6 +30,7 @@
 import android.view.View
 import android.view.View.OnLongClickListener
 import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.view.accessibility.AccessibilityEvent
 import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.TextView
@@ -263,7 +264,11 @@
 
     override fun onHandleMenuOpened() {}
 
-    override fun onHandleMenuClosed() {}
+    override fun onHandleMenuClosed() {
+        openMenuButton.post {
+            openMenuButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+        }
+    }
 
     fun setAnimatingTaskResizeOrReposition(animatingTaskResizeOrReposition: Boolean) {
         // If animating a task resize or reposition, cancel any running hover animations
@@ -309,6 +314,12 @@
         )
     }
 
+    fun requestAccessibilityFocus() {
+        maximizeWindowButton.post {
+            maximizeWindowButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
+        }
+    }
+
     private fun getHeaderStyle(header: Header): HeaderStyle {
         return HeaderStyle(
             background = getHeaderBackground(header),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 74db912..f007115 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -650,7 +650,7 @@
                 .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
 
         mCloseMaxMenuRunnable.getValue().run();
-        verify(menu).close();
+        verify(menu).close(any());
         assertFalse(decoration.isMaximizeMenuActive());
     }
 
@@ -669,7 +669,7 @@
                 .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS));
 
         mCloseMaxMenuRunnable.getValue().run();
-        verify(menu).close();
+        verify(menu).close(any());
         assertFalse(decoration.isMaximizeMenuActive());
     }
 
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 9603c0a..d17a9b6 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -60,7 +60,7 @@
     method @FlaggedApi("android.nfc.nfc_oem_extension") @NonNull public java.util.List<java.lang.String> getActiveNfceeList();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
-    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOn(int);
+    method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void setControllerAlwaysOnMode(int);
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
     method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
     field @FlaggedApi("android.nfc.nfc_oem_extension") public static final int DISABLE = 0; // 0x0
diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java
index 2804546..951702c 100644
--- a/nfc/java/android/nfc/NfcAdapter.java
+++ b/nfc/java/android/nfc/NfcAdapter.java
@@ -560,13 +560,13 @@
     public @interface TagIntentAppPreferenceResult {}
 
     /**
-     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOnMode(int)}.
      * @hide
      */
     public static final int CONTROLLER_ALWAYS_ON_MODE_DEFAULT = 1;
 
     /**
-     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOn(int)}.
+     * Mode Type for {@link NfcOemExtension#setControllerAlwaysOnMode(int)}.
      * @hide
      */
     public static final int CONTROLLER_ALWAYS_ON_DISABLE = 0;
@@ -2323,7 +2323,7 @@
      * <p>This API is for the NFCC internal state management. It allows to discriminate
      * the controller function from the NFC function by keeping the NFC controller on without
      * any NFC RF enabled if necessary.
-     * <p>This call is asynchronous. Register a listener {@link #ControllerAlwaysOnListener}
+     * <p>This call is asynchronous. Register a listener {@link ControllerAlwaysOnListener}
      * by {@link #registerControllerAlwaysOnListener} to find out when the operation is
      * complete.
      * <p>If this returns true, then either NFCC always on state has been set based on the value,
@@ -2337,7 +2337,7 @@
      * FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
      * FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC and FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE
      * are unavailable
-     * @return true if feature is supported by the device and operation has bee initiated,
+     * @return true if feature is supported by the device and operation has been initiated,
      * false if the feature is not supported by the device.
      * @hide
      */
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 45038d4..011c60b 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -70,7 +70,7 @@
     private boolean mRfDiscoveryStarted = false;
 
     /**
-     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
      * Enables the controller in default mode when NFC is disabled (existing API behavior).
      * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
      * @hide
@@ -80,7 +80,7 @@
     public static final int ENABLE_DEFAULT = NfcAdapter.CONTROLLER_ALWAYS_ON_MODE_DEFAULT;
 
     /**
-     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
      * Enables the controller in transparent mode when NFC is disabled.
      * @hide
      */
@@ -89,7 +89,7 @@
     public static final int ENABLE_TRANSPARENT = 2;
 
     /**
-     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
      * Enables the controller and initializes and enables the EE subsystem when NFC is disabled.
      * @hide
      */
@@ -98,7 +98,7 @@
     public static final int ENABLE_EE = 3;
 
     /**
-     * Mode Type for {@link #setControllerAlwaysOn(int)}.
+     * Mode Type for {@link #setControllerAlwaysOnMode(int)}.
      * Disable the Controller Always On Mode.
      * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}.
      * @hide
@@ -108,7 +108,7 @@
     public static final int DISABLE = NfcAdapter.CONTROLLER_ALWAYS_ON_DISABLE;
 
     /**
-     * Possible controller modes for {@link #setControllerAlwaysOn(int)}.
+     * Possible controller modes for {@link #setControllerAlwaysOnMode(int)}.
      *
      * @hide
      */
@@ -449,6 +449,9 @@
      * <p>This call is asynchronous, register listener {@link NfcAdapter.ControllerAlwaysOnListener}
      * by {@link NfcAdapter#registerControllerAlwaysOnListener} to find out when the operation is
      * complete.
+     * <p> Note: This adds more always on modes on top of existing
+     * {@link NfcAdapter#setControllerAlwaysOn(boolean)} API which can be used to set the NFCC in
+     * only {@link #ENABLE_DEFAULT} and {@link #DISABLE} modes.
      * @param mode one of {@link ControllerMode} modes
      * @throws UnsupportedOperationException if
      *   <li> if FEATURE_NFC, FEATURE_NFC_HOST_CARD_EMULATION, FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
@@ -456,11 +459,12 @@
      *   are unavailable </li>
      *   <li> if the feature is unavailable @see NfcAdapter#isNfcControllerAlwaysOnSupported() </li>
      * @hide
+     * @see NfcAdapter#setControllerAlwaysOn(boolean)
      */
     @SystemApi
     @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
     @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON)
-    public void setControllerAlwaysOn(@ControllerMode int mode) {
+    public void setControllerAlwaysOnMode(@ControllerMode int mode) {
         if (!NfcAdapter.sHasNfcFeature && !NfcAdapter.sHasCeFeature) {
             throw new UnsupportedOperationException();
         }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index f64c305..749ad0a 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -967,7 +967,7 @@
         for (int i = 0; i < nameCount; i++) {
             String name = names.get(i);
             Setting setting = settingsState.getSettingLocked(name);
-            pw.print("_id:"); pw.print(toDumpString(setting.getId()));
+            pw.print("_id:"); pw.print(toDumpString(String.valueOf(setting.getId())));
             pw.print(" name:"); pw.print(toDumpString(name));
             if (setting.getPackageName() != null) {
                 pw.print(" pkg:"); pw.print(setting.getPackageName());
@@ -2785,7 +2785,7 @@
 
             switch (column) {
                 case Settings.NameValueTable._ID -> {
-                    values[i] = setting.getId();
+                    values[i] = String.valueOf(setting.getId());
                 }
                 case Settings.NameValueTable.NAME -> {
                     values[i] = setting.getName();
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index 4165339..3c634f0 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -567,7 +567,7 @@
         }
         try {
             localCounter = Integer.parseInt(markerSetting.value);
-        } catch(NumberFormatException e) {
+        } catch (NumberFormatException e) {
             // reset local counter
             markerSetting.value = "0";
         }
@@ -1364,7 +1364,10 @@
                     }
 
                     try {
-                        if (writeSingleSetting(mVersion, serializer, setting.getId(),
+                        if (writeSingleSetting(
+                                mVersion,
+                                serializer,
+                                Long.toString(setting.getId()),
                                 setting.getName(),
                                 setting.getValue(), setting.getDefaultValue(),
                                 setting.getPackageName(),
@@ -1633,7 +1636,7 @@
             TypedXmlPullParser parser = Xml.resolvePullParser(in);
             parseStateLocked(parser);
             return true;
-        } catch (XmlPullParserException | IOException e) {
+        } catch (XmlPullParserException | IOException | NumberFormatException e) {
             Slog.e(LOG_TAG, "parse settings xml failed", e);
             return false;
         } finally {
@@ -1653,7 +1656,7 @@
     }
 
     private void parseStateLocked(TypedXmlPullParser parser)
-            throws IOException, XmlPullParserException {
+            throws IOException, XmlPullParserException, NumberFormatException {
         final int outerDepth = parser.getDepth();
         int type;
         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -1709,7 +1712,7 @@
 
     @GuardedBy("mLock")
     private void parseSettingsLocked(TypedXmlPullParser parser)
-            throws IOException, XmlPullParserException {
+            throws IOException, XmlPullParserException, NumberFormatException {
 
         mVersion = parser.getAttributeInt(null, ATTR_VERSION);
 
@@ -1777,7 +1780,7 @@
                     }
                 }
                 mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag,
-                        fromSystem, id, isPreservedInRestore));
+                        fromSystem, Long.valueOf(id), isPreservedInRestore));
 
                 if (DEBUG_PERSISTENCE) {
                     Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
@@ -1867,7 +1870,7 @@
         private String value;
         private String defaultValue;
         private String packageName;
-        private String id;
+        private long id;
         private String tag;
         // Whether the default is set by the system
         private boolean defaultFromSystem;
@@ -1899,30 +1902,27 @@
         }
 
         public Setting(String name, String value, String defaultValue,
-                String packageName, String tag, boolean fromSystem, String id) {
+                String packageName, String tag, boolean fromSystem, long id) {
             this(name, value, defaultValue, packageName, tag, fromSystem, id,
                     /* isOverrideableByRestore */ false);
         }
 
         Setting(String name, String value, String defaultValue,
-                String packageName, String tag, boolean fromSystem, String id,
+                String packageName, String tag, boolean fromSystem, long id,
                 boolean isValuePreservedInRestore) {
-            mNextId = Math.max(mNextId, Long.parseLong(id) + 1);
-            if (NULL_VALUE.equals(value)) {
-                value = null;
-            }
+            mNextId = Math.max(mNextId, id + 1);
             init(name, value, tag, defaultValue, packageName, fromSystem, id,
                     isValuePreservedInRestore);
         }
 
         private void init(String name, String value, String tag, String defaultValue,
-                String packageName, boolean fromSystem, String id,
+                String packageName, boolean fromSystem, long id,
                 boolean isValuePreservedInRestore) {
             this.name = name;
-            this.value = value;
+            this.value = internValue(value);
             this.tag = tag;
-            this.defaultValue = defaultValue;
-            this.packageName = packageName;
+            this.defaultValue = internValue(defaultValue);
+            this.packageName = TextUtils.safeIntern(packageName);
             this.id = id;
             this.defaultFromSystem = fromSystem;
             this.isValuePreservedInRestore = isValuePreservedInRestore;
@@ -1960,7 +1960,7 @@
             return isValuePreservedInRestore;
         }
 
-        public String getId() {
+        public long getId() {
             return id;
         }
 
@@ -1993,9 +1993,6 @@
         private boolean update(String value, boolean setDefault, String packageName, String tag,
                 boolean forceNonSystemPackage, boolean overrideableByRestore,
                 boolean resetToDefault) {
-            if (NULL_VALUE.equals(value)) {
-                value = null;
-            }
             final boolean callerSystem = !forceNonSystemPackage &&
                     !isNull() && (isCalledFromSystem(packageName)
                     || isSystemPackage(mContext, packageName));
@@ -2040,7 +2037,7 @@
             }
 
             init(name, value, tag, defaultValue, packageName, defaultFromSystem,
-                    String.valueOf(mNextId++), isPreserved);
+                    mNextId++, isPreserved);
 
             return true;
         }
@@ -2052,6 +2049,32 @@
                     + " defaultFromSystem=" + defaultFromSystem + "}";
         }
 
+        /**
+         * Interns a string if it's a common setting value.
+         * Otherwise returns the given string.
+         */
+        static String internValue(String str) {
+            if (str == null) {
+                return null;
+            }
+            switch (str) {
+                case "true":
+                    return "true";
+                case "false":
+                    return "false";
+                case "0":
+                    return "0";
+                case "1":
+                    return "1";
+                case "":
+                    return "";
+                case "null":
+                    return null;  // explicit null has special handling
+                default:
+                    return str;
+            }
+        }
+
         private boolean shouldPreserveSetting(boolean overrideableByRestore,
                 boolean resetToDefault, String packageName, String value) {
             if (resetToDefault) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
index aaf49ff..9444664 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt
@@ -42,8 +42,9 @@
     isClock: Boolean = false,
 ): Modifier {
     val translationYState = remember { mutableStateOf(0F) }
-    val copiedParams = params.copy(translationY = { translationYState.value })
-    val burnIn = viewModel.movement(copiedParams)
+    viewModel.updateBurnInParams(params.copy(translationY = { translationYState.value }))
+
+    val burnIn = viewModel.movement
     val translationX by
         burnIn.map { it.translationX.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f)
     val translationY by
@@ -51,12 +52,7 @@
     translationYState.value = translationY
     val scaleViewModel by
         burnIn
-            .map {
-                BurnInScaleViewModel(
-                    scale = it.scale,
-                    scaleClockOnly = it.scaleClockOnly,
-                )
-            }
+            .map { BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) }
             .collectAsStateWithLifecycle(initialValue = BurnInScaleViewModel())
 
     return this.graphicsLayer {
@@ -72,8 +68,6 @@
 
 /** Reports the "top" coordinate of the modified composable to the given [consumer]. */
 @Composable
-fun Modifier.onTopPlacementChanged(
-    consumer: (Float) -> Unit,
-): Modifier {
+fun Modifier.onTopPlacementChanged(consumer: (Float) -> Unit): Modifier {
     return onPlaced { coordinates -> consumer(coordinates.boundsInWindow().top) }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 1b99a96..fe4a65b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -130,8 +130,8 @@
 fun SceneScope.HeadsUpNotificationSpace(
     stackScrollView: NotificationScrollView,
     viewModel: NotificationsPlaceholderViewModel,
+    useHunBounds: () -> Boolean = { true },
     modifier: Modifier = Modifier,
-    isPeekFromBottom: Boolean = false,
 ) {
     Box(
         modifier =
@@ -141,17 +141,25 @@
                 .notificationHeadsUpHeight(stackScrollView)
                 .debugBackground(viewModel, DEBUG_HUN_COLOR)
                 .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                    val positionInWindow = coordinates.positionInWindow()
-                    val boundsInWindow = coordinates.boundsInWindow()
-                    debugLog(viewModel) {
-                        "HUNS onGloballyPositioned:" +
-                            " size=${coordinates.size}" +
-                            " bounds=$boundsInWindow"
+                    // This element is sometimes opted out of the shared element system, so there
+                    // can be multiple instances of it during a transition. Thus we need to
+                    // determine which instance should feed its bounds to NSSL to avoid providing
+                    // conflicting values
+                    val useBounds = useHunBounds()
+                    if (useBounds) {
+                        val positionInWindow = coordinates.positionInWindow()
+                        val boundsInWindow = coordinates.boundsInWindow()
+                        debugLog(viewModel) {
+                            "HUNS onGloballyPositioned:" +
+                                " size=${coordinates.size}" +
+                                " bounds=$boundsInWindow"
+                        }
+                        // Note: boundsInWindow doesn't scroll off the screen, so use
+                        // positionInWindow
+                        // for top bound, which can scroll off screen while snoozing
+                        stackScrollView.setHeadsUpTop(positionInWindow.y)
+                        stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
                     }
-                    // Note: boundsInWindow doesn't scroll off the screen, so use positionInWindow
-                    // for top bound, which can scroll off screen while snoozing
-                    stackScrollView.setHeadsUpTop(positionInWindow.y)
-                    stackScrollView.setHeadsUpBottom(boundsInWindow.bottom)
                 }
     )
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index d91958a..0c69dbd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -416,11 +416,11 @@
         HeadsUpNotificationSpace(
             stackScrollView = notificationStackScrollView,
             viewModel = notificationsPlaceholderViewModel,
+            useHunBounds = { shouldUseQuickSettingsHunBounds(layoutState.transitionState) },
             modifier =
                 Modifier.align(Alignment.BottomCenter)
                     .navigationBarsPadding()
                     .padding(horizontal = shadeHorizontalPadding),
-            isPeekFromBottom = true,
         )
         NotificationScrollingStack(
             shadeSession = shadeSession,
@@ -446,3 +446,7 @@
         )
     }
 }
+
+private fun shouldUseQuickSettingsHunBounds(state: TransitionState): Boolean {
+    return state is TransitionState.Idle && state.currentScene == Scenes.QuickSettings
+}
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 f64d0ed..58fbf43 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
@@ -77,6 +77,10 @@
     }
     from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
     from(Scenes.Lockscreen, to = Scenes.Gone) { lockscreenToGoneTransition() }
+    from(Scenes.QuickSettings, to = Scenes.Shade) {
+        reversed { shadeToQuickSettingsTransition() }
+        sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
+    }
     from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
 
     // Overlay transitions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 4bc71fd..75a77cf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -27,17 +27,6 @@
 import android.hardware.face.FaceSensorPropertiesInternal
 import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
-import com.android.keyguard.keyguardUpdateMonitor
-import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
-import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.res.R
-import com.android.systemui.util.mockito.whenever
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 
 /** Create [FingerprintSensorPropertiesInternal] for a test. */
 internal fun fingerprintSensorPropertiesInternal(
@@ -156,67 +145,3 @@
     info.negativeButtonText = negativeButton
     return info
 }
-
-@OptIn(ExperimentalCoroutinesApi::class)
-internal fun TestScope.updateSfpsIndicatorRequests(
-    kosmos: Kosmos,
-    mContext: SysuiTestableContext,
-    primaryBouncerRequest: Boolean? = null,
-    alternateBouncerRequest: Boolean? = null,
-    biometricPromptRequest: Boolean? = null,
-    // TODO(b/365182034): update when rest to unlock feature is implemented
-    //    progressBarShowing: Boolean? = null
-) {
-    biometricPromptRequest?.let { hasBiometricPromptRequest ->
-        if (hasBiometricPromptRequest) {
-            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
-                AuthenticationReason.BiometricPromptAuthentication
-            )
-        } else {
-            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
-                AuthenticationReason.NotRunning
-            )
-        }
-    }
-
-    primaryBouncerRequest?.let { hasPrimaryBouncerRequest ->
-        updatePrimaryBouncer(
-            kosmos,
-            mContext,
-            isShowing = hasPrimaryBouncerRequest,
-            isAnimatingAway = false,
-            fpsDetectionRunning = true,
-            isUnlockingWithFpAllowed = true
-        )
-    }
-
-    alternateBouncerRequest?.let { hasAlternateBouncerRequest ->
-        kosmos.keyguardBouncerRepository.setAlternateVisible(hasAlternateBouncerRequest)
-    }
-
-    // TODO(b/365182034): set progress bar visibility when rest to unlock feature is implemented
-
-    runCurrent()
-}
-
-internal fun updatePrimaryBouncer(
-    kosmos: Kosmos,
-    mContext: SysuiTestableContext,
-    isShowing: Boolean,
-    isAnimatingAway: Boolean,
-    fpsDetectionRunning: Boolean,
-    isUnlockingWithFpAllowed: Boolean,
-) {
-    kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
-    kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
-    val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
-    kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
-        primaryStartDisappearAnimation
-    )
-
-    whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
-        .thenReturn(fpsDetectionRunning)
-    whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
-        .thenReturn(isUnlockingWithFpAllowed)
-    mContext.orCreateTestableResources.addOverride(R.bool.config_show_sidefps_hint_on_bouncer, true)
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
deleted file mode 100644
index 298b54a..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ /dev/null
@@ -1,174 +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.biometrics.domain.interactor
-
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.biometricStatusRepository
-import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
-import com.android.systemui.biometrics.shared.model.AuthenticationReason
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.updateSfpsIndicatorRequests
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.display.data.repository.displayRepository
-import com.android.systemui.display.data.repository.displayStateRepository
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-class SideFpsOverlayInteractorTest : SysuiTestCase() {
-    private val kosmos = testKosmos()
-    private val underTest = kosmos.sideFpsOverlayInteractor
-
-    @Test
-    fun verifyIsShowingFalse_whenInRearDisplayMode() {
-        kosmos.testScope.runTest {
-            val isShowing by collectLastValue(underTest.isShowing)
-            setupTestConfiguration(isInRearDisplayMode = true)
-
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
-            runCurrent()
-
-            assertThat(isShowing).isFalse()
-        }
-    }
-
-    @Test
-    fun verifyIsShowingUpdates_onPrimaryBouncerShowAndHide() {
-        kosmos.testScope.runTest {
-            val isShowing by collectLastValue(underTest.isShowing)
-            setupTestConfiguration(isInRearDisplayMode = false)
-
-            // Show primary bouncer
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
-            runCurrent()
-
-            assertThat(isShowing).isTrue()
-
-            // Hide primary bouncer
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = false)
-            runCurrent()
-
-            assertThat(isShowing).isFalse()
-        }
-    }
-
-    @Test
-    fun verifyIsShowingUpdates_onAlternateBouncerShowAndHide() {
-        kosmos.testScope.runTest {
-            val isShowing by collectLastValue(underTest.isShowing)
-            setupTestConfiguration(isInRearDisplayMode = false)
-
-            updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = true)
-            runCurrent()
-
-            assertThat(isShowing).isTrue()
-
-            // Hide alternate bouncer
-            updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = false)
-            runCurrent()
-
-            assertThat(isShowing).isFalse()
-        }
-    }
-
-    @Test
-    fun verifyIsShowingUpdates_onSystemServerAuthenticationStartedAndStopped() {
-        kosmos.testScope.runTest {
-            val isShowing by collectLastValue(underTest.isShowing)
-            setupTestConfiguration(isInRearDisplayMode = false)
-
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
-            runCurrent()
-
-            assertThat(isShowing).isTrue()
-
-            // System server authentication stopped
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = false)
-            runCurrent()
-
-            assertThat(isShowing).isFalse()
-        }
-    }
-
-    // On progress bar shown - hide indicator
-    // On progress bar hidden - show indicator
-    // TODO(b/365182034): update + enable when rest to unlock feature is implemented
-    @Ignore("b/365182034")
-    @Test
-    fun verifyIsShowingUpdates_onProgressBarInteraction() {
-        kosmos.testScope.runTest {
-            val isShowing by collectLastValue(underTest.isShowing)
-            setupTestConfiguration(isInRearDisplayMode = false)
-
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
-            runCurrent()
-
-            assertThat(isShowing).isTrue()
-
-            //            updateSfpsIndicatorRequests(
-            //                kosmos, mContext, primaryBouncerRequest = true, progressBarShowing =
-            // true
-            //            )
-            runCurrent()
-
-            assertThat(isShowing).isFalse()
-
-            // Set progress bar invisible
-            //            updateSfpsIndicatorRequests(
-            //                kosmos, mContext, primaryBouncerRequest = true, progressBarShowing =
-            // false
-            //            )
-            runCurrent()
-
-            // Verify indicator shown
-            assertThat(isShowing).isTrue()
-        }
-    }
-
-    private suspend fun TestScope.setupTestConfiguration(isInRearDisplayMode: Boolean) {
-        kosmos.fingerprintPropertyRepository.setProperties(
-            sensorId = 1,
-            strength = SensorStrength.STRONG,
-            sensorType = FingerprintSensorType.POWER_BUTTON,
-            sensorLocations = emptyMap()
-        )
-
-        kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
-        kosmos.displayRepository.emitDisplayChangeEvent(0)
-        runCurrent()
-
-        kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
-            AuthenticationReason.NotRunning
-        )
-        // TODO(b/365182034): set progress bar visibility once rest to unlock feature is implemented
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 2eea668..7fa165c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -16,48 +16,64 @@
 
 package com.android.systemui.biometrics.ui.binder
 
+import android.animation.Animator
+import android.graphics.Rect
+import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManagerGlobal
 import android.testing.TestableLooper
+import android.view.Display
+import android.view.DisplayInfo
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewPropertyAnimator
+import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowMetrics
 import android.view.layoutInflater
 import android.view.windowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.LottieAnimationView
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.data.repository.biometricStatusRepository
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
 import com.android.systemui.biometrics.shared.model.DisplayRotation
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.updateSfpsIndicatorRequests
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
 import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.display.data.repository.displayStateRepository
+import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito
 import org.mockito.Mockito.any
 import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.firstValue
+import org.mockito.kotlin.argumentCaptor
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -67,25 +83,84 @@
     private val kosmos = testKosmos()
 
     @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
+    @Mock private lateinit var displayManager: DisplayManager
+    @Mock
+    private lateinit var fingerprintInteractiveToAuthProvider: FingerprintInteractiveToAuthProvider
     @Mock private lateinit var layoutInflater: LayoutInflater
     @Mock private lateinit var sideFpsView: View
-    @Captor private lateinit var viewCaptor: ArgumentCaptor<View>
+
+    private val contextDisplayInfo = DisplayInfo()
+
+    private var displayWidth: Int = 0
+    private var displayHeight: Int = 0
+    private var boundsWidth: Int = 0
+    private var boundsHeight: Int = 0
+
+    private lateinit var deviceConfig: DeviceConfig
+    private lateinit var sensorLocation: SensorLocationInternal
+
+    enum class DeviceConfig {
+        X_ALIGNED,
+        Y_ALIGNED,
+    }
 
     @Before
     fun setup() {
         allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
+
+        mContext = spy(mContext)
+
+        val resources = mContext.resources
+        whenever(mContext.display)
+            .thenReturn(
+                Display(mock(DisplayManagerGlobal::class.java), 1, contextDisplayInfo, resources)
+            )
+
         kosmos.layoutInflater = layoutInflater
+
+        whenever(fingerprintInteractiveToAuthProvider.enabledForCurrentUser)
+            .thenReturn(MutableStateFlow(false))
+
+        context.addMockSystemService(DisplayManager::class.java, displayManager)
         context.addMockSystemService(WindowManager::class.java, kosmos.windowManager)
+
         `when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
         `when`(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
             .thenReturn(mock(LottieAnimationView::class.java))
+        with(mock(ViewPropertyAnimator::class.java)) {
+            `when`(sideFpsView.animate()).thenReturn(this)
+            `when`(alpha(Mockito.anyFloat())).thenReturn(this)
+            `when`(setStartDelay(Mockito.anyLong())).thenReturn(this)
+            `when`(setDuration(Mockito.anyLong())).thenReturn(this)
+            `when`(setListener(any())).thenAnswer {
+                (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
+                    mock(Animator::class.java)
+                )
+                this
+            }
+        }
     }
 
     @Test
     fun verifyIndicatorNotAdded_whenInRearDisplayMode() {
         kosmos.testScope.runTest {
-            setupTestConfiguration(isInRearDisplayMode = true)
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = true
+            )
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            runCurrent()
+
             verify(kosmos.windowManager, never()).addView(any(), any())
         }
     }
@@ -93,14 +168,33 @@
     @Test
     fun verifyIndicatorShowAndHide_onPrimaryBouncerShowAndHide() {
         kosmos.testScope.runTest {
-            setupTestConfiguration(isInRearDisplayMode = false)
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+            // Show primary bouncer
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             verify(kosmos.windowManager).addView(any(), any())
 
             // Hide primary bouncer
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = false)
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             verify(kosmos.windowManager).removeView(any())
@@ -110,19 +204,30 @@
     @Test
     fun verifyIndicatorShowAndHide_onAlternateBouncerShowAndHide() {
         kosmos.testScope.runTest {
-            setupTestConfiguration(isInRearDisplayMode = false)
-            updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = true)
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+            // Show alternate bouncer
+            kosmos.keyguardBouncerRepository.setAlternateVisible(true)
             runCurrent()
 
             verify(kosmos.windowManager).addView(any(), any())
 
+            var viewCaptor = argumentCaptor<View>()
             verify(kosmos.windowManager).addView(viewCaptor.capture(), any())
             verify(viewCaptor.firstValue)
                 .announceForAccessibility(
                     mContext.getText(R.string.accessibility_side_fingerprint_indicator_label)
                 )
 
-            updateSfpsIndicatorRequests(kosmos, mContext, alternateBouncerRequest = false)
+            // Hide alternate bouncer
+            kosmos.keyguardBouncerRepository.setAlternateVisible(false)
             runCurrent()
 
             verify(kosmos.windowManager).removeView(any())
@@ -132,14 +237,30 @@
     @Test
     fun verifyIndicatorShownAndHidden_onSystemServerAuthenticationStartedAndStopped() {
         kosmos.testScope.runTest {
-            setupTestConfiguration(isInRearDisplayMode = false)
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
+            // System server authentication started
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
             runCurrent()
 
             verify(kosmos.windowManager).addView(any(), any())
 
             // System server authentication stopped
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = false)
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
             runCurrent()
 
             verify(kosmos.windowManager).removeView(any())
@@ -148,35 +269,45 @@
 
     // On progress bar shown - hide indicator
     // On progress bar hidden - show indicator
-    // TODO(b/365182034): update + enable when rest to unlock feature is implemented
-    @Ignore("b/365182034")
     @Test
     fun verifyIndicatorProgressBarInteraction() {
         kosmos.testScope.runTest {
             // Pre-auth conditions
-            setupTestConfiguration(isInRearDisplayMode = false)
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
+            setupTestConfiguration(
+                DeviceConfig.X_ALIGNED,
+                rotation = DisplayRotation.ROTATION_0,
+                isInRearDisplayMode = false
+            )
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+
+            // Show primary bouncer
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             val inOrder = inOrder(kosmos.windowManager)
+
             // Verify indicator shown
             inOrder.verify(kosmos.windowManager).addView(any(), any())
 
             // Set progress bar visible
-            //            updateSfpsIndicatorRequests(
-            //                kosmos, mContext, primaryBouncerRequest = true, progressBarShowing =
-            // true
-            //            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(true)
+
             runCurrent()
 
             // Verify indicator hidden
             inOrder.verify(kosmos.windowManager).removeView(any())
 
             // Set progress bar invisible
-            //            updateSfpsIndicatorRequests(
-            //                kosmos, mContext, primaryBouncerRequest = true, progressBarShowing =
-            // false
-            //            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+
             runCurrent()
 
             // Verify indicator shown
@@ -184,18 +315,78 @@
         }
     }
 
-    private suspend fun TestScope.setupTestConfiguration(isInRearDisplayMode: Boolean) {
+    private fun updatePrimaryBouncer(
+        isShowing: Boolean,
+        isAnimatingAway: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+    ) {
+        kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
+        kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
+        val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+        kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
+            primaryStartDisappearAnimation
+        )
+
+        whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        mContext.orCreateTestableResources.addOverride(
+            R.bool.config_show_sidefps_hint_on_bouncer,
+            true
+        )
+    }
+
+    private suspend fun TestScope.setupTestConfiguration(
+        deviceConfig: DeviceConfig,
+        rotation: DisplayRotation = DisplayRotation.ROTATION_0,
+        isInRearDisplayMode: Boolean,
+    ) {
+        this@SideFpsOverlayViewBinderTest.deviceConfig = deviceConfig
+
+        when (deviceConfig) {
+            DeviceConfig.X_ALIGNED -> {
+                displayWidth = 3000
+                displayHeight = 1500
+                boundsWidth = 200
+                boundsHeight = 100
+                sensorLocation = SensorLocationInternal("", 2500, 0, boundsWidth / 2)
+            }
+            DeviceConfig.Y_ALIGNED -> {
+                displayWidth = 2500
+                displayHeight = 2000
+                boundsWidth = 100
+                boundsHeight = 200
+                sensorLocation = SensorLocationInternal("", displayWidth, 300, boundsHeight / 2)
+            }
+        }
+
+        whenever(kosmos.windowManager.maximumWindowMetrics)
+            .thenReturn(
+                WindowMetrics(
+                    Rect(0, 0, displayWidth, displayHeight),
+                    mock(WindowInsets::class.java),
+                )
+            )
+
+        contextDisplayInfo.uniqueId = DISPLAY_ID
+
         kosmos.fingerprintPropertyRepository.setProperties(
             sensorId = 1,
             strength = SensorStrength.STRONG,
             sensorType = FingerprintSensorType.POWER_BUTTON,
-            sensorLocations = emptyMap()
+            sensorLocations = mapOf(DISPLAY_ID to sensorLocation)
         )
 
         kosmos.displayStateRepository.setIsInRearDisplayMode(isInRearDisplayMode)
-        kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
+        kosmos.displayStateRepository.setCurrentRotation(rotation)
         kosmos.displayRepository.emitDisplayChangeEvent(0)
         kosmos.sideFpsOverlayViewBinder.start()
         runCurrent()
     }
+
+    companion object {
+        private const val DISPLAY_ID = "displayId"
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 27b1371..0db7b62 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -30,19 +30,23 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.model.KeyPath
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.settingslib.Utils
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.data.repository.biometricStatusRepository
 import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
 import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason
 import com.android.systemui.biometrics.shared.model.DisplayRotation
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.LottieCallback
 import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.updateSfpsIndicatorRequests
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.display.data.repository.displayRepository
 import com.android.systemui.display.data.repository.displayStateRepository
+import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
 import com.android.systemui.testKosmos
@@ -280,7 +284,17 @@
         kosmos.testScope.runTest {
             val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
 
-            updateSfpsIndicatorRequests(kosmos, mContext, primaryBouncerRequest = true)
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.NotRunning
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = true,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             assertThat(lottieCallbacks)
@@ -298,7 +312,17 @@
             val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
             setDarkMode(true)
 
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             assertThat(lottieCallbacks)
@@ -314,7 +338,17 @@
             val lottieCallbacks by collectLastValue(kosmos.sideFpsOverlayViewModel.lottieCallbacks)
             setDarkMode(false)
 
-            updateSfpsIndicatorRequests(kosmos, mContext, biometricPromptRequest = true)
+            kosmos.biometricStatusRepository.setFingerprintAuthenticationReason(
+                AuthenticationReason.BiometricPromptAuthentication
+            )
+            kosmos.sideFpsProgressBarViewModel.setVisible(false)
+
+            updatePrimaryBouncer(
+                isShowing = false,
+                isAnimatingAway = false,
+                fpsDetectionRunning = true,
+                isUnlockingWithFpAllowed = true
+            )
             runCurrent()
 
             assertThat(lottieCallbacks)
@@ -337,6 +371,29 @@
         mContext.resources.configuration.uiMode = uiMode
     }
 
+    private fun updatePrimaryBouncer(
+        isShowing: Boolean,
+        isAnimatingAway: Boolean,
+        fpsDetectionRunning: Boolean,
+        isUnlockingWithFpAllowed: Boolean,
+    ) {
+        kosmos.keyguardBouncerRepository.setPrimaryShow(isShowing)
+        kosmos.keyguardBouncerRepository.setPrimaryStartingToHide(false)
+        val primaryStartDisappearAnimation = if (isAnimatingAway) Runnable {} else null
+        kosmos.keyguardBouncerRepository.setPrimaryStartDisappearAnimation(
+            primaryStartDisappearAnimation
+        )
+
+        whenever(kosmos.keyguardUpdateMonitor.isFingerprintDetectionRunning)
+            .thenReturn(fpsDetectionRunning)
+        whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithFingerprintAllowed)
+            .thenReturn(isUnlockingWithFpAllowed)
+        mContext.orCreateTestableResources.addOverride(
+            R.bool.config_show_sidefps_hint_on_bouncer,
+            true
+        )
+    }
+
     private suspend fun TestScope.setupTestConfiguration(
         deviceConfig: DeviceConfig,
         rotation: DisplayRotation = DisplayRotation.ROTATION_0,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
index ff6ea3a..40a9add 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt
@@ -91,12 +91,13 @@
         kosmos.fakeKeyguardClockRepository.setCurrentClock(clockController)
 
         underTest = kosmos.aodBurnInViewModel
+        underTest.updateBurnInParams(burnInParameters)
     }
 
     @Test
     fun movement_initializedToDefaultValues() =
         testScope.runTest {
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            val movement by collectLastValue(underTest.movement)
             assertThat(movement?.translationY).isEqualTo(0)
             assertThat(movement?.translationX).isEqualTo(0)
             assertThat(movement?.scale).isEqualTo(1f)
@@ -105,7 +106,7 @@
     @Test
     fun translationAndScale_whenNotDozing() =
         testScope.runTest {
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to not dozing (on lockscreen)
             keyguardTransitionRepository.sendTransitionStep(
@@ -130,8 +131,8 @@
     @Test
     fun translationAndScale_whenFullyDozing() =
         testScope.runTest {
-            burnInParameters = burnInParameters.copy(minViewY = 100)
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to dozing (on AOD)
             keyguardTransitionRepository.sendTransitionStep(
@@ -171,8 +172,8 @@
     @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() =
         testScope.runTest {
-            burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to dozing (on AOD)
             keyguardTransitionRepository.sendTransitionStep(
@@ -213,8 +214,8 @@
     @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
     fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() =
         testScope.runTest {
-            burnInParameters = burnInParameters.copy(minViewY = 100, topInset = 80)
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to dozing (on AOD)
             keyguardTransitionRepository.sendTransitionStep(
@@ -256,7 +257,7 @@
         testScope.runTest {
             whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
 
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to dozing (on AOD)
             keyguardTransitionRepository.sendTransitionStep(
@@ -374,7 +375,7 @@
             whenever(clockController.config.useAlternateSmartspaceAODTransition)
                 .thenReturn(if (isWeatherClock) true else false)
 
-            val movement by collectLastValue(underTest.movement(burnInParameters))
+            val movement by collectLastValue(underTest.movement)
 
             // Set to dozing (on AOD)
             keyguardTransitionRepository.sendTransitionStep(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 2fd94e2..5d606c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -47,10 +47,14 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.phone.screenOffAnimationController
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
@@ -73,7 +77,7 @@
 @EnableFlags(
     FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT,
     FLAG_NEW_AOD_TRANSITION,
-    FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
+    FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR,
 )
 class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
     private val kosmos = testKosmos()
@@ -110,6 +114,20 @@
     @Before
     fun setUp() {
         kosmos.sceneContainerRepository.setTransitionState(transitionState)
+
+        // Add sample notif so that the notif shelf has something to display
+        kosmos.activeNotificationListRepository.activeNotifications.value =
+            ActiveNotificationsStore.Builder()
+                .apply {
+                    addIndividualNotif(
+                        activeNotificationModel(
+                            key = "notif",
+                            aodIcon = mock(),
+                            groupKey = "testGroup",
+                        )
+                    )
+                }
+                .build()
     }
 
     @Test
@@ -129,7 +147,7 @@
                     from = KeyguardState.LOCKSCREEN,
                     to = KeyguardState.AOD,
                     value = 0f,
-                    transitionState = TransitionState.STARTED
+                    transitionState = TransitionState.STARTED,
                 ),
                 validateStep = false,
             )
@@ -393,7 +411,7 @@
                     flowOf(Scenes.Communal),
                     flowOf(0.5f),
                     false,
-                    emptyFlow()
+                    emptyFlow(),
                 )
 
             keyguardTransitionRepository.sendTransitionSteps(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 4adf693..add7ac9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -55,7 +55,6 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
-import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
@@ -70,7 +69,6 @@
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
 import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -155,7 +153,7 @@
     fun setUp() {
         shadeTestUtil.setSplitShade(false)
         movementFlow = MutableStateFlow(BurnInModel())
-        whenever(aodBurnInViewModel.movement(any())).thenReturn(movementFlow)
+        whenever(aodBurnInViewModel.movement).thenReturn(movementFlow)
         underTest = kosmos.sharedNotificationContainerViewModel
     }
 
@@ -810,7 +808,7 @@
     @DisableSceneContainer
     fun translationYUpdatesOnKeyguardForBurnIn() =
         testScope.runTest {
-            val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
+            val translationY by collectLastValue(underTest.translationY)
 
             showLockscreen()
             assertThat(translationY).isEqualTo(0)
@@ -823,7 +821,7 @@
     @DisableSceneContainer
     fun translationYUpdatesOnKeyguard() =
         testScope.runTest {
-            val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
+            val translationY by collectLastValue(underTest.translationY)
 
             configurationRepository.setDimensionPixelSize(
                 R.dimen.keyguard_translate_distance_on_swipe_up,
@@ -844,7 +842,7 @@
     @DisableSceneContainer
     fun translationYDoesNotUpdateWhenShadeIsExpanded() =
         testScope.runTest {
-            val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
+            val translationY by collectLastValue(underTest.translationY)
 
             configurationRepository.setDimensionPixelSize(
                 R.dimen.keyguard_translate_distance_on_swipe_up,
diff --git a/packages/SystemUI/plugin/Android.bp b/packages/SystemUI/plugin/Android.bp
index 682a68f..a26cf12 100644
--- a/packages/SystemUI/plugin/Android.bp
+++ b/packages/SystemUI/plugin/Android.bp
@@ -23,9 +23,7 @@
 }
 
 java_library {
-
     name: "SystemUIPluginLib",
-
     srcs: [
         "bcsmartspace/src/**/*.java",
         "bcsmartspace/src/**/*.kt",
@@ -40,6 +38,8 @@
         export_proguard_flags_files: true,
     },
 
+    plugins: ["PluginAnnotationProcessor"],
+
     // If you add a static lib here, you may need to also add the package to the ClassLoaderFilter
     // in PluginInstance. That will ensure that loaded plugins have access to the related classes.
     // You should also add it to proguard_common.flags so that proguard does not remove the portions
@@ -53,7 +53,6 @@
         "SystemUILogLib",
         "androidx.annotation_annotation",
     ],
-
 }
 
 android_app {
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/TestPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/TestPlugin.kt
new file mode 100644
index 0000000..33f7b7a
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/TestPlugin.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins
+
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import com.android.systemui.plugins.annotations.ProvidesInterface
+
+@ProtectedInterface
+@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
+/** Interface intended for use in tests */
+interface TestPlugin : Plugin {
+    companion object {
+        const val VERSION = 1
+
+        const val ACTION = "testAction"
+    }
+
+    @ProtectedReturn("return new Object();")
+    /** Test method, implemented by test */
+    fun methodThrowsError(): Object
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8dc4815..6d27b6f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -21,7 +21,11 @@
 import com.android.internal.annotations.Keep
 import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.Plugin
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -31,6 +35,7 @@
 typealias ClockId = String
 
 /** A Plugin which exposes the ClockProvider interface */
+@ProtectedInterface
 @ProvidesInterface(action = ClockProviderPlugin.ACTION, version = ClockProviderPlugin.VERSION)
 interface ClockProviderPlugin : Plugin, ClockProvider {
     companion object {
@@ -40,31 +45,42 @@
 }
 
 /** Interface for building clocks and providing information about those clocks */
+@ProtectedInterface
+@GeneratedImport("java.util.List")
+@GeneratedImport("java.util.ArrayList")
 interface ClockProvider {
     /** Initializes the clock provider with debug log buffers */
     fun initialize(buffers: ClockMessageBuffers?)
 
+    @ProtectedReturn("return new ArrayList<ClockMetadata>();")
     /** Returns metadata for all clocks this provider knows about */
     fun getClocks(): List<ClockMetadata>
 
+    @ProtectedReturn("return null;")
     /** Initializes and returns the target clock design */
-    fun createClock(settings: ClockSettings): ClockController
+    fun createClock(settings: ClockSettings): ClockController?
 
+    @ProtectedReturn("return new ClockPickerConfig(\"\", \"\", \"\", null);")
     /** Settings configuration parameters for the clock */
     fun getClockPickerConfig(id: ClockId): ClockPickerConfig
 }
 
 /** Interface for controlling an active clock */
+@ProtectedInterface
 interface ClockController {
+    @get:SimpleProperty
     /** A small version of the clock, appropriate for smaller viewports */
     val smallClock: ClockFaceController
 
+    @get:SimpleProperty
     /** A large version of the clock, appropriate when a bigger viewport is available */
     val largeClock: ClockFaceController
 
+    @get:SimpleProperty
     /** Determines the way the hosting app should behave when rendering either clock face */
     val config: ClockConfig
 
+    @get:SimpleProperty
     /** Events that clocks may need to respond to */
     val events: ClockEvents
 
@@ -76,19 +92,26 @@
 }
 
 /** Interface for a specific clock face version rendered by the clock */
+@ProtectedInterface
 interface ClockFaceController {
+    @get:SimpleProperty
+    @Deprecated("Prefer use of layout")
     /** View that renders the clock face */
     val view: View
 
+    @get:SimpleProperty
     /** Layout specification for this clock */
     val layout: ClockFaceLayout
 
+    @get:SimpleProperty
     /** Determines the way the hosting app should behave when rendering this clock face */
     val config: ClockFaceConfig
 
+    @get:SimpleProperty
     /** Events specific to this clock face */
     val events: ClockFaceEvents
 
+    @get:SimpleProperty
     /** Triggers for various animations */
     val animations: ClockAnimations
 }
@@ -107,14 +130,21 @@
 
 data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
 
-/** Specifies layout information for the */
+/** Specifies layout information for the clock face */
+@ProtectedInterface
+@GeneratedImport("java.util.ArrayList")
+@GeneratedImport("android.view.View")
 interface ClockFaceLayout {
+    @get:ProtectedReturn("return new ArrayList<View>();")
     /** All clock views to add to the root constraint layout before applying constraints. */
     val views: List<View>
 
+    @ProtectedReturn("return constraints;")
     /** Custom constraints to apply to Lockscreen ConstraintLayout. */
     fun applyConstraints(constraints: ConstraintSet): ConstraintSet
 
+    @ProtectedReturn("return constraints;")
+    /** Custom constraints to apply to preview ConstraintLayout. */
     fun applyPreviewConstraints(constraints: ConstraintSet): ConstraintSet
 
     fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
@@ -145,7 +175,9 @@
 }
 
 /** Events that should call when various rendering parameters change */
+@ProtectedInterface
 interface ClockEvents {
+    @get:ProtectedReturn("return false;")
     /** Set to enable or disable swipe interaction */
     var isReactiveTouchInteractionEnabled: Boolean
 
@@ -187,6 +219,7 @@
 )
 
 /** Methods which trigger various clock animations */
+@ProtectedInterface
 interface ClockAnimations {
     /** Runs an enter animation (if any) */
     fun enter()
@@ -230,6 +263,7 @@
 }
 
 /** Events that have specific data about the related face */
+@ProtectedInterface
 interface ClockFaceEvents {
     /** Call every time tick */
     fun onTimeTick()
@@ -270,7 +304,9 @@
 /** Some data about a clock design */
 data class ClockMetadata(val clockId: ClockId)
 
-data class ClockPickerConfig(
+data class ClockPickerConfig
+@JvmOverloads
+constructor(
     val id: String,
 
     /** Localized name of the clock */
@@ -338,7 +374,7 @@
     /** Transition to AOD should move smartspace like large clock instead of small clock */
     val useAlternateSmartspaceAODTransition: Boolean = false,
 
-    /** Use ClockPickerConfig.isReactiveToTone instead */
+    /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
     @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
     val isReactiveToTone: Boolean = true,
 
diff --git a/packages/SystemUI/plugin_core/Android.bp b/packages/SystemUI/plugin_core/Android.bp
index 521c019..31fbda5 100644
--- a/packages/SystemUI/plugin_core/Android.bp
+++ b/packages/SystemUI/plugin_core/Android.bp
@@ -24,8 +24,13 @@
 
 java_library {
     sdk_version: "current",
-    name: "PluginCoreLib",
-    srcs: ["src/**/*.java"],
+    name: "PluginAnnotationLib",
+    host_supported: true,
+    device_supported: true,
+    srcs: [
+        "src/**/annotations/*.java",
+        "src/**/annotations/*.kt",
+    ],
     optimize: {
         proguard_flags_files: ["proguard.flags"],
         // Ensure downstream clients that reference this as a shared lib
@@ -37,3 +42,59 @@
     // no compatibility issues with launcher
     java_version: "1.8",
 }
+
+java_library {
+    sdk_version: "current",
+    name: "PluginCoreLib",
+    device_supported: true,
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+    exclude_srcs: [
+        "src/**/annotations/*.java",
+        "src/**/annotations/*.kt",
+        "src/**/processor/*.java",
+        "src/**/processor/*.kt",
+    ],
+    static_libs: [
+        "PluginAnnotationLib",
+    ],
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+        // Ensure downstream clients that reference this as a shared lib
+        // inherit the appropriate flags to preserve annotations.
+        export_proguard_flags_files: true,
+    },
+
+    // Enforce that the library is built against java 8 so that there are
+    // no compatibility issues with launcher
+    java_version: "1.8",
+}
+
+java_library {
+    java_version: "1.8",
+    name: "PluginAnnotationProcessorLib",
+    host_supported: true,
+    device_supported: false,
+    srcs: [
+        "src/**/processor/*.java",
+        "src/**/processor/*.kt",
+    ],
+    plugins: ["auto_service_plugin"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "auto_service_annotations",
+        "auto_common",
+        "PluginAnnotationLib",
+        "guava",
+        "jsr330",
+    ],
+}
+
+java_plugin {
+    name: "PluginAnnotationProcessor",
+    processor_class: "com.android.systemui.plugins.processor.ProtectedPluginProcessor",
+    static_libs: ["PluginAnnotationProcessorLib"],
+    java_version: "1.8",
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
index 8ff6c11..84040f9 100644
--- a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java
@@ -15,6 +15,7 @@
 
 import android.content.Context;
 
+import com.android.systemui.plugins.annotations.ProtectedReturn;
 import com.android.systemui.plugins.annotations.Requires;
 
 /**
@@ -116,6 +117,8 @@
      * @deprecated
      * @see Requires
      */
+    @Deprecated
+    @ProtectedReturn(statement = "return -1;")
     default int getVersion() {
         // Default of -1 indicates the plugin supports the new Requires model.
         return -1;
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginWrapper.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginWrapper.kt
new file mode 100644
index 0000000..debb318
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/PluginWrapper.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.plugins
+
+/** [PluginWrapper] wraps an interface used by a plugin */
+interface PluginWrapper<T> {
+    /** Instance that is being wrapped */
+    fun getPlugin(): T
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
new file mode 100644
index 0000000..3a1f251
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/ProtectedPluginListener.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package com.android.systemui.plugins
+
+/** Listener for events from proxy types generated by [ProtectedPluginProcessor]. */
+interface ProtectedPluginListener {
+    /**
+     * Called when a method call produces a [LinkageError] before returning. This callback is
+     * provided so that the host application can terminate the plugin or log the error as
+     * appropriate.
+     *
+     * @return true to terminate all methods within this object; false if the error is recoverable
+     *   and the proxied plugin should continue to operate as normal.
+     */
+    fun onFail(className: String, methodName: String, failure: LinkageError): Boolean
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
new file mode 100644
index 0000000..12a977d
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/annotations/ProtectedInterface.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.plugins.annotations
+
+/**
+ * This annotation marks denotes that an interface should use a proxy layer to protect the plugin
+ * host from crashing due to [LinkageError]s originating within the plugin's implementation.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedInterface
+
+/**
+ * This annotation specifies any additional imports that the processor will require when generating
+ * the proxy implementation for the target interface. The interface in question must still be
+ * annotated with [ProtectedInterface].
+ */
+@Repeatable
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.BINARY)
+annotation class GeneratedImport(val extraImport: String)
+
+/**
+ * This annotation provides default values to return when the proxy implementation catches a
+ * [LinkageError]. The string specified should be a simple but valid java statement. In most cases
+ * it should be a return statement of the appropriate type, but in some cases throwing a known
+ * exception type may be preferred.
+ *
+ * This annotation is not required for methods that return void, but will behave the same way.
+ */
+@Target(
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY,
+    AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class ProtectedReturn(val statement: String)
+
+/**
+ * Some very simple properties and methods need not be protected by the proxy implementation. This
+ * annotation can be used to omit the normal try-catch wrapper the proxy is using. These members
+ * will instead be a direct passthrough.
+ *
+ * It should only be used for members where the plugin implementation is expected to be exceedingly
+ * simple. Any member marked with this annotation should be no more complex than kotlin's automatic
+ * properties, and make no other method calls whatsoever.
+ */
+@Target(
+    AnnotationTarget.FUNCTION,
+    AnnotationTarget.PROPERTY,
+    AnnotationTarget.PROPERTY_GETTER,
+    AnnotationTarget.PROPERTY_SETTER,
+)
+@Retention(AnnotationRetention.BINARY)
+annotation class SimpleProperty
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
new file mode 100644
index 0000000..6b54d89
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/ProtectedPluginProcessor.kt
@@ -0,0 +1,356 @@
+/*
+ * 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.plugins.processor
+
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import com.android.systemui.plugins.annotations.SimpleProperty
+import com.google.auto.service.AutoService
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.PackageElement
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic.Kind
+import kotlin.collections.ArrayDeque
+
+/**
+ * [ProtectedPluginProcessor] generates a proxy implementation for interfaces annotated with
+ * [ProtectedInterface] which catches [LinkageError]s generated by the proxied target. This protects
+ * the plugin host from crashing due to out-of-date plugin code, where some call has changed so that
+ * the [ClassLoader] can no longer resolve it correctly.
+ *
+ * [PluginInstance] observes these failures via [ProtectedMethodListener] and unloads the plugin in
+ * question to prevent further issues. This persists through further load/unload requests.
+ *
+ * To centralize access to the proxy types, an additional type [PluginProtector] is also generated.
+ * This class provides static methods which wrap an instance of the target interface in the proxy
+ * type if it is not already an instance of the proxy.
+ */
+@AutoService(ProtectedPluginProcessor::class)
+class ProtectedPluginProcessor : AbstractProcessor() {
+    private lateinit var procEnv: ProcessingEnvironment
+
+    override fun init(procEnv: ProcessingEnvironment) {
+        this.procEnv = procEnv
+    }
+
+    override fun getSupportedAnnotationTypes(): Set<String> =
+        setOf("com.android.systemui.plugins.annotations.ProtectedInterface")
+
+    private data class TargetData(
+        val attribute: TypeElement,
+        val sourceType: Element,
+        val sourcePkg: String,
+        val sourceName: String,
+        val outputName: String,
+    )
+
+    override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+        val targets = mutableMapOf<String, TargetData>() // keyed by fully-qualified source name
+        val additionalImports = mutableSetOf<String>()
+        for (attr in annotations) {
+            for (target in roundEnv.getElementsAnnotatedWith(attr)) {
+                val sourceName = "${target.simpleName}"
+                val outputName = "${sourceName}Protector"
+                val pkg = (target.getEnclosingElement() as PackageElement).qualifiedName.toString()
+                targets.put("$target", TargetData(attr, target, pkg, sourceName, outputName))
+
+                // This creates excessive imports, but it should be fine
+                additionalImports.add("$pkg.$sourceName")
+                additionalImports.add("$pkg.$outputName")
+            }
+        }
+
+        if (targets.size <= 0) return false
+        for ((_, sourceType, sourcePkg, sourceName, outputName) in targets.values) {
+            // Find all methods in this type and all super types to that need to be implemented
+            val types = ArrayDeque<TypeMirror>().apply { addLast(sourceType.asType()) }
+            val impAttrs = mutableListOf<GeneratedImport>()
+            val methods = mutableListOf<ExecutableElement>()
+            while (types.size > 0) {
+                val typeMirror = types.removeLast()
+                if (typeMirror.toString() == "java.lang.Object") continue
+                val type = procEnv.typeUtils.asElement(typeMirror)
+                for (member in type.enclosedElements) {
+                    if (member.kind != ElementKind.METHOD) continue
+                    methods.add(member as ExecutableElement)
+                }
+
+                impAttrs.addAll(type.getAnnotationsByType(GeneratedImport::class.java))
+                types.addAll(procEnv.typeUtils.directSupertypes(typeMirror))
+            }
+
+            val file = procEnv.filer.createSourceFile("$outputName")
+            TabbedWriter.writeTo(file.openWriter()) {
+                line("package $sourcePkg;")
+                line()
+
+                // Imports used by the proxy implementation
+                line("import android.util.Log;")
+                line("import java.lang.LinkageError;")
+                line("import com.android.systemui.plugins.PluginWrapper;")
+                line("import com.android.systemui.plugins.ProtectedPluginListener;")
+                line()
+
+                // Imports of other generated types
+                if (additionalImports.size > 0) {
+                    for (impTarget in additionalImports) {
+                        line("import $impTarget;")
+                    }
+                    line()
+                }
+
+                // Imports declared via @GeneratedImport
+                if (impAttrs.size > 0) {
+                    for (impAttr in impAttrs) {
+                        line("import ${impAttr.extraImport};")
+                    }
+                    line()
+                }
+
+                val interfaces = "$sourceName, PluginWrapper<$sourceName>"
+                braceBlock("public class $outputName implements $interfaces") {
+                    line("private static final String CLASS = \"$sourceName\";")
+
+                    // Static factory method to prevent wrapping the same object twice
+                    parenBlock("public static $outputName protect") {
+                        line("$sourceName instance,")
+                        line("ProtectedPluginListener listener")
+                    }
+                    braceBlock {
+                        line("if (instance instanceof $outputName)")
+                        line("    return ($outputName)instance;")
+                        line("return new $outputName(instance, listener);")
+                    }
+                    line()
+
+                    // Member Fields
+                    line("private $sourceName mInstance;")
+                    line("private ProtectedPluginListener mListener;")
+                    line("private boolean mHasError = false;")
+                    line()
+
+                    // Constructor
+                    parenBlock("private $outputName") {
+                        line("$sourceName instance,")
+                        line("ProtectedPluginListener listener")
+                    }
+                    braceBlock {
+                        line("mInstance = instance;")
+                        line("mListener = listener;")
+                    }
+                    line()
+
+                    // Wrapped instance getter for version checker
+                    braceBlock("public $sourceName getPlugin()") { line("return mInstance;") }
+
+                    // Method implementations
+                    for (method in methods) {
+                        val methodName = method.simpleName
+                        val returnTypeName = method.returnType.toString()
+                        val callArgs = StringBuilder()
+                        var isFirst = true
+
+                        line("@Override")
+                        parenBlock("public $returnTypeName $methodName") {
+                            // While copying the method signature for the proxy type, we also
+                            // accumulate arguments for the nested callsite.
+                            for (param in method.parameters) {
+                                if (!isFirst) completeLine(",")
+                                startLine("${param.asType()} ${param.simpleName}")
+                                isFirst = false
+
+                                if (callArgs.length > 0) callArgs.append(", ")
+                                callArgs.append(param.simpleName)
+                            }
+                        }
+
+                        val isVoid = method.returnType.kind == TypeKind.VOID
+                        val nestedCall = "mInstance.$methodName($callArgs)"
+                        val callStatement =
+                            when {
+                                isVoid -> "$nestedCall;"
+                                targets.containsKey(returnTypeName) -> {
+                                    val targetType = targets.get(returnTypeName)!!.outputName
+                                    "return $targetType.protect($nestedCall, mListener);"
+                                }
+                                else -> "return $nestedCall;"
+                            }
+
+                        // Simple property methods forgo protection
+                        val simpleAttr = method.getAnnotation(SimpleProperty::class.java)
+                        if (simpleAttr != null) {
+                            braceBlock {
+                                line("final String METHOD = \"$methodName\";")
+                                line(callStatement)
+                            }
+                            line()
+                            continue
+                        }
+
+                        // Standard implementation wraps nested call in try-catch
+                        braceBlock {
+                            val retAttr = method.getAnnotation(ProtectedReturn::class.java)
+                            val errorStatement =
+                                when {
+                                    retAttr != null -> retAttr.statement
+                                    isVoid -> "return;"
+                                    else -> {
+                                        // Non-void methods must be annotated.
+                                        procEnv.messager.printMessage(
+                                            Kind.ERROR,
+                                            "$outputName.$methodName must be annotated with " +
+                                                "@ProtectedReturn or @SimpleProperty",
+                                        )
+                                        "throw ex;"
+                                    }
+                                }
+
+                            line("final String METHOD = \"$methodName\";")
+
+                            // Return immediately if any previous call has failed.
+                            braceBlock("if (mHasError)") { line(errorStatement) }
+
+                            // Protect callsite in try/catch block
+                            braceBlock("try") { line(callStatement) }
+
+                            // Notify listener when a LinkageError is caught
+                            braceBlock("catch (LinkageError ex)") {
+                                line("Log.wtf(CLASS, \"Failed to execute: \" + METHOD, ex);")
+                                line("mHasError = mListener.onFail(CLASS, METHOD, ex);")
+                                line(errorStatement)
+                            }
+                        }
+                        line()
+                    }
+                }
+            }
+        }
+
+        // Write a centralized static factory type to its own file. This is for convience so that
+        // PluginInstance need not resolve each generated type at runtime as plugins are loaded.
+        val factoryFile = procEnv.filer.createSourceFile("PluginProtector")
+        TabbedWriter.writeTo(factoryFile.openWriter()) {
+            line("package com.android.systemui.plugins;")
+            line()
+
+            line("import java.util.Map;")
+            line("import java.util.ArrayList;")
+            line("import java.util.HashSet;")
+            line("import android.util.Log;")
+            line("import static java.util.Map.entry;")
+            line()
+
+            for (impTarget in additionalImports) {
+                line("import $impTarget;")
+            }
+            line()
+
+            braceBlock("public final class PluginProtector") {
+                line("private PluginProtector() { }")
+                line()
+
+                line("private static final String TAG = \"PluginProtector\";")
+                line()
+
+                // Untyped factory SAM, private to this type.
+                braceBlock("private interface Factory") {
+                    line("Object create(Object plugin, ProtectedPluginListener listener);")
+                }
+                line()
+
+                // Store a reference to each `protect` method in a map by interface type.
+                parenBlock("private static final Map<Class, Factory> sFactories = Map.ofEntries") {
+                    var isFirst = true
+                    for (target in targets.values) {
+                        if (!isFirst) completeLine(",")
+                        target.apply {
+                            startLine("entry($sourceName.class, ")
+                            appendLine("(p, h) -> $outputName.protect(($sourceName)p, h))")
+                        }
+                        isFirst = false
+                    }
+                }
+                completeLine(";")
+                line()
+
+                // Lookup the relevant factory based on the instance type, if not found return null.
+                parenBlock("public static <T> T tryProtect") {
+                    line("T target,")
+                    line("ProtectedPluginListener listener")
+                }
+                braceBlock {
+                    // Accumulate interfaces from type and all base types
+                    line("HashSet<Class> interfaces = new HashSet<Class>();")
+                    line("Class current = target.getClass();")
+                    braceBlock("while (current != null)") {
+                        braceBlock("for (Class cls : current.getInterfaces())") {
+                            line("interfaces.add(cls);")
+                        }
+                        line("current = current.getSuperclass();")
+                    }
+                    line()
+
+                    // Check if any of the interfaces are marked protectable
+                    line("int candidateCount = 0;")
+                    line("Factory candidateFactory = null;")
+                    braceBlock("for (Class cls : interfaces)") {
+                        line("Factory factory = sFactories.get(cls);")
+                        braceBlock("if (factory != null)") {
+                            line("candidateFactory = factory;")
+                            line("candidateCount++;")
+                        }
+                    }
+                    line()
+
+                    // No match, return null
+                    braceBlock("if (candidateFactory == null)") {
+                        line("Log.i(TAG, \"Wasn't able to wrap \" + target);")
+                        line("return null;")
+                    }
+
+                    // Multiple matches, not supported
+                    braceBlock("if (candidateCount >= 2)") {
+                        var error = "Plugin implements more than one protected interface"
+                        line("throw new UnsupportedOperationException(\"$error\");")
+                    }
+
+                    // Call the factory and wrap the target object
+                    line("return (T)candidateFactory.create(target, listener);")
+                }
+                line()
+
+                // Wraps the target with the appropriate generated proxy if it exists.
+                parenBlock("public static <T> T protectIfAble") {
+                    line("T target,")
+                    line("ProtectedPluginListener listener")
+                }
+                braceBlock {
+                    line("T result = tryProtect(target, listener);")
+                    line("return result != null ? result : target;")
+                }
+                line()
+            }
+        }
+
+        return true
+    }
+}
diff --git a/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
new file mode 100644
index 0000000..941b2c2
--- /dev/null
+++ b/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/processor/TabbedWriter.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.plugins.processor
+
+import java.io.BufferedWriter
+import java.io.Writer
+
+/**
+ * [TabbedWriter] is a convience class which tracks and writes correctly tabbed lines for generating
+ * source files. These files don't need to be correctly tabbed as they're ephemeral and not part of
+ * the source tree, but correct tabbing makes debugging much easier when the build fails.
+ */
+class TabbedWriter(writer: Writer) : AutoCloseable {
+    private val target = BufferedWriter(writer)
+    private var isInProgress = false
+    var tabCount: Int = 0
+        private set
+
+    override fun close() = target.close()
+
+    fun line() {
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun line(str: String) {
+        if (isInProgress) {
+            target.newLine()
+        }
+
+        target.append("    ".repeat(tabCount))
+        target.append(str)
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun completeLine(str: String) {
+        if (!isInProgress) {
+            target.newLine()
+            target.append("    ".repeat(tabCount))
+        }
+
+        target.append(str)
+        target.newLine()
+        isInProgress = false
+    }
+
+    fun startLine(str: String) {
+        if (isInProgress) {
+            target.newLine()
+        }
+
+        target.append("    ".repeat(tabCount))
+        target.append(str)
+        isInProgress = true
+    }
+
+    fun appendLine(str: String) {
+        if (!isInProgress) {
+            target.append("    ".repeat(tabCount))
+        }
+
+        target.append(str)
+        isInProgress = true
+    }
+
+    fun braceBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+        block(str, " {", "}", true, write)
+    }
+
+    fun parenBlock(str: String = "", write: TabbedWriter.() -> Unit) {
+        block(str, "(", ")", false, write)
+    }
+
+    private fun block(
+        str: String,
+        start: String,
+        end: String,
+        newLineForEnd: Boolean,
+        write: TabbedWriter.() -> Unit,
+    ) {
+        appendLine(str)
+        completeLine(start)
+
+        tabCount++
+        this.write()
+        tabCount--
+
+        if (newLineForEnd) {
+            line(end)
+        } else {
+            startLine(end)
+        }
+    }
+
+    companion object {
+        fun writeTo(writer: Writer, write: TabbedWriter.() -> Unit) {
+            TabbedWriter(writer).use { it.write() }
+        }
+    }
+}
diff --git a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
index 91cd019..3b3ed39 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_one_pane_layout.xml
@@ -215,4 +215,17 @@
         app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintVertical_bias="1.0"
         tools:srcCompat="@tools:sample/avatars" />
+
+    <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+        android:id="@+id/biometric_icon_overlay"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_gravity="center"
+        android:contentDescription="@null"
+        android:scaleType="fitXY"
+        android:importantForAccessibility="no"
+        app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
+        app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
+        app:layout_constraintStart_toStartOf="@+id/biometric_icon"
+        app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
index 51117a7..2a00495 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_two_pane_layout.xml
@@ -40,6 +40,19 @@
         app:layout_constraintTop_toTopOf="parent"
         tools:srcCompat="@tools:sample/avatars" />
 
+    <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+        android:id="@+id/biometric_icon_overlay"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_gravity="center"
+        android:contentDescription="@null"
+        android:scaleType="fitXY"
+        android:importantForAccessibility="no"
+        app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
+        app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
+        app:layout_constraintStart_toStartOf="@+id/biometric_icon"
+        app:layout_constraintTop_toTopOf="@+id/biometric_icon" />
+
     <ScrollView
         android:id="@+id/scrollView"
         android:layout_width="0dp"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a3db776..24b6579 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1482,7 +1482,7 @@
     <!-- Text which is shown in the expanded notification shade when there are currently no notifications visible that the user hasn't already seen. [CHAR LIMIT=30] -->
     <string name="no_unseen_notif_text">No new notifications</string>
 
-    <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=50] -->
+    <!-- Title of heads up notification for adaptive notifications user education. [CHAR LIMIT=60] -->
     <string name="adaptive_notification_edu_hun_title">Notification cooldown is on</string>
 
     <!-- Text of heads up notification for adaptive notifications user education. [CHAR LIMIT=100] -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 87cc86f..8298397 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -32,6 +32,9 @@
 import com.android.systemui.plugins.PluginFragment;
 import com.android.systemui.plugins.PluginLifecycleManager;
 import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.PluginProtector;
+import com.android.systemui.plugins.PluginWrapper;
+import com.android.systemui.plugins.ProtectedPluginListener;
 
 import dalvik.system.PathClassLoader;
 
@@ -49,7 +52,8 @@
  *
  * @param <T> The type of plugin that this contains.
  */
-public class PluginInstance<T extends Plugin> implements PluginLifecycleManager {
+public class PluginInstance<T extends Plugin>
+        implements PluginLifecycleManager, ProtectedPluginListener {
     private static final String TAG = "PluginInstance";
 
     private final Context mAppContext;
@@ -58,6 +62,7 @@
     private final PluginFactory<T> mPluginFactory;
     private final String mTag;
 
+    private boolean mHasError = false;
     private BiConsumer<String, String> mLogConsumer = null;
     private Context mPluginContext;
     private T mPlugin;
@@ -87,6 +92,11 @@
         return mTag;
     }
 
+    /** */
+    public boolean hasError() {
+        return mHasError;
+    }
+
     public void setLogFunc(BiConsumer logConsumer) {
         mLogConsumer = logConsumer;
     }
@@ -97,8 +107,21 @@
         }
     }
 
+    @Override
+    public synchronized boolean onFail(String className, String methodName, LinkageError failure) {
+        mHasError = true;
+        unloadPlugin();
+        mListener.onPluginDetached(this);
+        return true;
+    }
+
     /** Alerts listener and plugin that the plugin has been created. */
     public synchronized void onCreate() {
+        if (mHasError) {
+            log("Previous LinkageError detected for plugin class");
+            return;
+        }
+
         boolean loadPlugin = mListener.onPluginAttached(this);
         if (!loadPlugin) {
             if (mPlugin != null) {
@@ -109,13 +132,17 @@
         }
 
         if (mPlugin == null) {
-            log("onCreate auto-load");
+            log("onCreate: auto-load");
             loadPlugin();
             return;
         }
 
+        if (!checkVersion()) {
+            log("onCreate: version check failed");
+            return;
+        }
+
         log("onCreate: load callbacks");
-        mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
             // will get the onCreate as part of the fragment lifecycle.
@@ -126,6 +153,12 @@
 
     /** Alerts listener and plugin that the plugin is being shutdown. */
     public synchronized void onDestroy() {
+        if (mHasError) {
+            // Detached in error handler
+            log("onDestroy - no-op");
+            return;
+        }
+
         log("onDestroy");
         unloadPlugin();
         mListener.onPluginDetached(this);
@@ -134,28 +167,37 @@
     /** Returns the current plugin instance (if it is loaded). */
     @Nullable
     public T getPlugin() {
-        return mPlugin;
+        return mHasError ? null : mPlugin;
     }
 
     /**
      * Loads and creates the plugin if it does not exist.
      */
     public synchronized void loadPlugin() {
+        if (mHasError) {
+            log("Previous LinkageError detected for plugin class");
+            return;
+        }
+
         if (mPlugin != null) {
             log("Load request when already loaded");
             return;
         }
 
         // Both of these calls take about 1 - 1.5 seconds in test runs
-        mPlugin = mPluginFactory.createPlugin();
+        mPlugin = mPluginFactory.createPlugin(this);
         mPluginContext = mPluginFactory.createPluginContext();
         if (mPlugin == null || mPluginContext == null) {
             Log.e(mTag, "Requested load, but failed");
             return;
         }
 
+        if (!checkVersion()) {
+            log("loadPlugin: version check failed");
+            return;
+        }
+
         log("Loaded plugin; running callbacks");
-        mPluginFactory.checkVersion(mPlugin);
         if (!(mPlugin instanceof PluginFragment)) {
             // Only call onCreate for plugins that aren't fragments, as fragments
             // will get the onCreate as part of the fragment lifecycle.
@@ -165,6 +207,29 @@
     }
 
     /**
+     * Checks the plugin version, and permanently destroys the plugin instance on a failure
+     */
+    private synchronized boolean checkVersion() {
+        if (mHasError) {
+            return false;
+        }
+
+        if (mPlugin == null) {
+            return true;
+        }
+
+        if (mPluginFactory.checkVersion(mPlugin)) {
+            return true;
+        }
+
+        Log.wtf(TAG, "Version check failed for " + mPlugin.getClass().getSimpleName());
+        mHasError = true;
+        unloadPlugin();
+        mListener.onPluginDetached(this);
+        return false;
+    }
+
+    /**
      * Unloads and destroys the current plugin instance if it exists.
      *
      * This will free the associated memory if there are not other references.
@@ -204,7 +269,7 @@
     }
 
     public VersionInfo getVersionInfo() {
-        return mPluginFactory.checkVersion(mPlugin);
+        return mPluginFactory.getVersionInfo(mPlugin);
     }
 
     @VisibleForTesting
@@ -295,16 +360,19 @@
 
     /** Class that compares a plugin class against an implementation for version matching. */
     public interface VersionChecker {
-        /** Compares two plugin classes. */
-        <T extends Plugin> VersionInfo checkVersion(
+        /** Compares two plugin classes. Returns true when match. */
+        <T extends Plugin> boolean checkVersion(
                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin);
+
+        /** Returns VersionInfo for the target class */
+        <T extends Plugin> VersionInfo getVersionInfo(Class<T> instanceclass);
     }
 
     /** Class that compares a plugin class against an implementation for version matching. */
     public static class VersionCheckerImpl implements VersionChecker {
         @Override
         /** Compares two plugin classes. */
-        public <T extends Plugin> VersionInfo checkVersion(
+        public <T extends Plugin> boolean checkVersion(
                 Class<T> instanceClass, Class<T> pluginClass, Plugin plugin) {
             VersionInfo pluginVersion = new VersionInfo().addClass(pluginClass);
             VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
@@ -313,11 +381,17 @@
             } else if (plugin != null) {
                 int fallbackVersion = plugin.getVersion();
                 if (fallbackVersion != pluginVersion.getDefaultVersion()) {
-                    throw new VersionInfo.InvalidVersionException("Invalid legacy version", false);
+                    return false;
                 }
-                return null;
             }
-            return instanceVersion;
+            return true;
+        }
+
+        @Override
+        /** Returns the version info for the class */
+        public <T extends Plugin> VersionInfo getVersionInfo(Class<T> instanceClass) {
+            VersionInfo instanceVersion = new VersionInfo().addClass(instanceClass);
+            return instanceVersion.hasVersionInfo() ? instanceVersion : null;
         }
     }
 
@@ -364,20 +438,16 @@
         }
 
         /** Creates the related plugin object from the factory */
-        public T createPlugin() {
+        public T createPlugin(ProtectedPluginListener listener) {
             try {
                 ClassLoader loader = mClassLoaderFactory.get();
                 Class<T> instanceClass = (Class<T>) Class.forName(
                         mComponentName.getClassName(), true, loader);
                 T result = (T) mInstanceFactory.create(instanceClass);
                 Log.v(TAG, "Created plugin: " + result);
-                return result;
-            } catch (ClassNotFoundException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
-            } catch (IllegalAccessException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
-            } catch (InstantiationException ex) {
-                Log.e(TAG, "Failed to load plugin", ex);
+                return PluginProtector.protectIfAble(result, listener);
+            } catch (ReflectiveOperationException ex) {
+                Log.wtf(TAG, "Failed to load plugin", ex);
             }
             return null;
         }
@@ -394,13 +464,27 @@
             return null;
         }
 
-        /** Check Version and create VersionInfo for instance */
-        public VersionInfo checkVersion(T instance) {
+        /** Check Version for the instance */
+        public boolean checkVersion(T instance) {
             if (instance == null) {
-                instance = createPlugin();
+                instance = createPlugin(null);
+            }
+            if (instance instanceof PluginWrapper) {
+                instance = ((PluginWrapper<T>) instance).getPlugin();
             }
             return mVersionChecker.checkVersion(
                     (Class<T>) instance.getClass(), mPluginClass, instance);
         }
+
+        /** Get Version Info for the instance */
+        public VersionInfo getVersionInfo(T instance) {
+            if (instance == null) {
+                instance = createPlugin(null);
+            }
+            if (instance instanceof PluginWrapper) {
+                instance = ((PluginWrapper<T>) instance).getPlugin();
+            }
+            return mVersionChecker.getVersionInfo((Class<T>) instance.getClass());
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index 0898134..76df9c9 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -25,7 +25,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.os.RemoteException;
-import android.util.Log;
 import android.view.GestureDetector;
 import android.view.ISystemGestureExclusionListener;
 import android.view.IWindowManager;
@@ -76,10 +75,9 @@
  * touches are consumed.
  */
 public class TouchMonitor {
+    private final Logger mLogger;
     // This executor is used to protect {@code mActiveTouchSessions} from being modified
     // concurrently. Any operation that adds or removes values should use this executor.
-    public String TAG = "DreamOverlayTouchMonitor";
-    private final Logger mLogger;
     private final Executor mMainExecutor;
     private final Executor mBackgroundExecutor;
 
@@ -298,13 +296,12 @@
                     mWindowManagerService.registerSystemGestureExclusionListener(
                             mGestureExclusionListener, mDisplayId);
                 } catch (RemoteException e) {
-                    // Handle the exception
-                    Log.e(TAG, "Failed to register gesture exclusion listener", e);
+                    mLogger.e("Failed to register gesture exclusion listener", e);
                 }
             });
         }
         mCurrentInputSession = mInputSessionFactory.create(
-                        "dreamOverlay",
+                        mLoggingName,
                         mInputEventListener,
                         mOnGestureListener,
                         true)
@@ -326,7 +323,7 @@
                     }
                 } catch (RemoteException e) {
                     // Handle the exception
-                    Log.e(TAG, "unregisterSystemGestureExclusionListener: failed", e);
+                    mLogger.e("unregisterSystemGestureExclusionListener: failed", e);
                 }
             });
         }
@@ -543,6 +540,7 @@
     private InputSession mCurrentInputSession;
     private final int mDisplayId;
     private final IWindowManager mWindowManagerService;
+    private final String mLoggingName;
 
     private Rect mMaxBounds;
 
@@ -589,7 +587,8 @@
         mDisplayHelper = displayHelper;
         mWindowManagerService = windowManagerService;
         mConfigurationInteractor = configurationInteractor;
-        mLogger = new Logger(logBuffer, loggingName + ":TouchMonitor");
+        mLoggingName = loggingName + ":TouchMonitor";
+        mLogger = new Logger(logBuffer, mLoggingName);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
index 7ecbb88..ec3fd9f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt
@@ -25,8 +25,6 @@
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
 import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
 import dagger.Module
@@ -48,12 +46,6 @@
 
     @Binds
     @SysUISingleton
-    fun providesSideFpsOverlayInteractor(
-        impl: SideFpsOverlayInteractorImpl
-    ): SideFpsOverlayInteractor
-
-    @Binds
-    @SysUISingleton
     fun providesCredentialInteractor(impl: CredentialInteractorImpl): CredentialInteractor
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
deleted file mode 100644
index 10c3483..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ /dev/null
@@ -1,82 +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.biometrics.domain.interactor
-
-import android.util.Log
-import com.android.systemui.biometrics.shared.model.AuthenticationReason.NotRunning
-import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
-import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.onEach
-
-/** Encapsulates business logic for showing and hiding the side fingerprint sensor indicator. */
-interface SideFpsOverlayInteractor {
-    /** Whether the side fingerprint sensor indicator is currently showing. */
-    val isShowing: Flow<Boolean>
-}
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class SideFpsOverlayInteractorImpl
-@Inject
-constructor(
-    biometricStatusInteractor: BiometricStatusInteractor,
-    displayStateInteractor: DisplayStateInteractor,
-    deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor,
-    sfpsSensorInteractor: SideFpsSensorInteractor,
-    // TODO(b/365182034): add progress bar input when rest to unlock feature is implemented
-) : SideFpsOverlayInteractor {
-    private val sfpsOverlayEnabled: Flow<Boolean> =
-        sfpsSensorInteractor.isAvailable.sample(displayStateInteractor.isInRearDisplayMode) {
-            isAvailable: Boolean,
-            isInRearDisplayMode: Boolean ->
-            isAvailable && !isInRearDisplayMode
-        }
-
-    private val showSideFpsOverlay: Flow<Boolean> =
-        combine(
-            biometricStatusInteractor.sfpsAuthenticationReason,
-            deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry,
-            // TODO(b/365182034): add progress bar input when rest to unlock feature is implemented
-        ) { systemServerAuthReason, showIndicatorForDeviceEntry ->
-            Log.d(
-                TAG,
-                "systemServerAuthReason = $systemServerAuthReason, " +
-                    "showIndicatorForDeviceEntry = $showIndicatorForDeviceEntry, "
-            )
-            systemServerAuthReason != NotRunning || showIndicatorForDeviceEntry
-        }
-
-    override val isShowing: Flow<Boolean> =
-        sfpsOverlayEnabled
-            .flatMapLatest { sfpsOverlayEnabled ->
-                if (!sfpsOverlayEnabled) {
-                    flowOf(false)
-                } else {
-                    showSideFpsOverlay
-                }
-            }
-            .onEach { Log.d(TAG, "isShowing: $it") }
-
-    companion object {
-        private const val TAG = "SideFpsOverlayInteractor"
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index d055731..73f75a4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -18,11 +18,13 @@
 
 import android.animation.Animator
 import android.animation.AnimatorSet
+import android.animation.ValueAnimator
 import android.graphics.Outline
 import android.graphics.Rect
 import android.transition.AutoTransition
 import android.transition.TransitionManager
 import android.util.TypedValue
+import android.view.Surface
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewOutlineProvider
@@ -158,13 +160,16 @@
             fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
                 viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
                 largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+                largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
                 largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
                 largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
 
                 if (hideSensorIcon) {
                     smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+                    smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
                     smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
                     mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+                    mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
                     mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 9fe1dc5..9578da4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -33,44 +33,89 @@
 import com.android.app.animation.Interpolators
 import com.android.keyguard.KeyguardPINView
 import com.android.systemui.CoreStartable
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
+import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
+import com.android.systemui.biometrics.shared.model.AuthenticationReason.NotRunning
 import com.android.systemui.biometrics.shared.model.LottieCallback
 import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.sample
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class SideFpsOverlayViewBinder
 @Inject
 constructor(
     @Application private val applicationScope: CoroutineScope,
     @Application private val applicationContext: Context,
+    private val biometricStatusInteractor: Lazy<BiometricStatusInteractor>,
+    private val displayStateInteractor: Lazy<DisplayStateInteractor>,
+    private val deviceEntrySideFpsOverlayInteractor: Lazy<DeviceEntrySideFpsOverlayInteractor>,
     private val layoutInflater: Lazy<LayoutInflater>,
-    private val sideFpsOverlayInteractor: Lazy<SideFpsOverlayInteractor>,
-    private val sideFpsOverlayViewModel: Lazy<SideFpsOverlayViewModel>,
+    private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
+    private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
     private val windowManager: Lazy<WindowManager>
 ) : CoreStartable {
-    private var overlayView: View? = null
 
     override fun start() {
-        applicationScope.launch {
-            sideFpsOverlayInteractor.get().isShowing.collect { isShowing: Boolean ->
-                if (isShowing) {
-                    show()
-                } else {
-                    hide()
+        applicationScope
+            .launch {
+                sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
+                    if (isSfpsAvailable) {
+                        combine(
+                                biometricStatusInteractor.get().sfpsAuthenticationReason,
+                                deviceEntrySideFpsOverlayInteractor
+                                    .get()
+                                    .showIndicatorForDeviceEntry,
+                                sideFpsProgressBarViewModel.get().isVisible,
+                                ::Triple
+                            )
+                            .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
+                            .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
+                                val (
+                                    systemServerAuthReason,
+                                    showIndicatorForDeviceEntry,
+                                    progressBarIsVisible) =
+                                    combinedFlows
+                                Log.d(
+                                    TAG,
+                                    "systemServerAuthReason = $systemServerAuthReason, " +
+                                        "showIndicatorForDeviceEntry = " +
+                                        "$showIndicatorForDeviceEntry, " +
+                                        "progressBarIsVisible = $progressBarIsVisible"
+                                )
+                                if (!isInRearDisplayMode) {
+                                    if (progressBarIsVisible) {
+                                        hide()
+                                    } else if (systemServerAuthReason != NotRunning) {
+                                        show()
+                                    } else if (showIndicatorForDeviceEntry) {
+                                        show()
+                                    } else {
+                                        hide()
+                                    }
+                                }
+                            }
+                    }
                 }
             }
-        }
     }
 
+    private var overlayView: View? = null
+
     /** Show the side fingerprint sensor indicator */
     private fun show() {
         if (overlayView?.isAttachedToWindow == true) {
@@ -80,10 +125,17 @@
             )
             return
         }
-        overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false)
-        val overlayViewModel = sideFpsOverlayViewModel.get()
-        bind(overlayView!!, overlayViewModel, windowManager.get())
 
+        overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false)
+
+        val overlayViewModel =
+            SideFpsOverlayViewModel(
+                applicationContext,
+                deviceEntrySideFpsOverlayInteractor.get(),
+                displayStateInteractor.get(),
+                sfpsSensorInteractor.get(),
+            )
+        bind(overlayView!!, overlayViewModel, windowManager.get())
         overlayView!!.visibility = View.INVISIBLE
         Log.d(TAG, "show(): adding overlayView $overlayView")
         windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
@@ -109,20 +161,6 @@
     companion object {
         private const val TAG = "SideFpsOverlayViewBinder"
 
-        private val accessibilityDelegate =
-            object : View.AccessibilityDelegate() {
-                override fun dispatchPopulateAccessibilityEvent(
-                    host: View,
-                    event: AccessibilityEvent
-                ): Boolean {
-                    return if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
-                        true
-                    } else {
-                        super.dispatchPopulateAccessibilityEvent(host, event)
-                    }
-                }
-            }
-
         /** Binds overlayView (side fingerprint sensor indicator view) to SideFpsOverlayViewModel */
         fun bind(
             overlayView: View,
@@ -146,7 +184,24 @@
 
                 overlayShowAnimator.start()
 
-                it.accessibilityDelegate = accessibilityDelegate
+                it.setAccessibilityDelegate(
+                    object : View.AccessibilityDelegate() {
+                        override fun dispatchPopulateAccessibilityEvent(
+                            host: View,
+                            event: AccessibilityEvent
+                        ): Boolean {
+                            return if (
+                                event.getEventType() ===
+                                    android.view.accessibility.AccessibilityEvent
+                                        .TYPE_WINDOW_STATE_CHANGED
+                            ) {
+                                true
+                            } else {
+                                super.dispatchPopulateAccessibilityEvent(host, event)
+                            }
+                        }
+                    }
+                )
 
                 repeatOnLifecycle(Lifecycle.State.STARTED) {
                     launch {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 85f221f..168ba11 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -78,11 +78,11 @@
 class PromptViewModel
 @Inject
 constructor(
-    private val displayStateInteractor: DisplayStateInteractor,
+    displayStateInteractor: DisplayStateInteractor,
     private val promptSelectorInteractor: PromptSelectorInteractor,
     @Application private val context: Context,
-    udfpsOverlayInteractor: UdfpsOverlayInteractor,
-    biometricStatusInteractor: BiometricStatusInteractor,
+    private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
+    private val biometricStatusInteractor: BiometricStatusInteractor,
     private val udfpsUtils: UdfpsUtils,
     private val iconProvider: IconProvider,
     private val activityTaskManager: ActivityTaskManager,
@@ -135,13 +135,11 @@
             R.dimen.biometric_prompt_landscape_medium_horizontal_padding
         )
 
-    val currentRotation: StateFlow<DisplayRotation> = displayStateInteractor.currentRotation
-
     val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> =
         udfpsOverlayInteractor.udfpsOverlayParams
 
     private val udfpsSensorBounds: Flow<Rect> =
-        combine(udfpsOverlayParams, currentRotation) { params, rotation ->
+        combine(udfpsOverlayParams, displayStateInteractor.currentRotation) { params, rotation ->
                 val rotatedBounds = Rect(params.sensorBounds)
                 RotationUtils.rotateBounds(
                     rotatedBounds,
@@ -264,7 +262,7 @@
                 _forceLargeSize,
                 promptKind,
                 displayStateInteractor.isLargeScreen,
-                currentRotation,
+                displayStateInteractor.currentRotation,
                 modalities
             ) { forceLarge, promptKind, isLargeScreen, rotation, modalities ->
                 when {
@@ -456,7 +454,7 @@
 
     /** Padding for prompt UI elements */
     val promptPadding: Flow<Rect> =
-        combine(size, currentRotation) { size, rotation ->
+        combine(size, displayStateInteractor.currentRotation) { size, rotation ->
             if (size != PromptSize.LARGE) {
                 val navBarInsets = Utils.getNavbarInsets(context)
                 if (rotation == DisplayRotation.ROTATION_90) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
index 7c1984e..c2a4ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
@@ -147,7 +147,8 @@
             _lottieBounds,
             sensorLocation,
             displayRotation,
-        ) { _: Rect?, sensorLocation: SideFpsSensorLocation, _: DisplayRotation ->
+        ) { bounds: Rect?, sensorLocation: SideFpsSensorLocation, displayRotation: DisplayRotation
+            ->
             val topLeft = Point(sensorLocation.left, sensorLocation.top)
 
             defaultOverlayViewParams.apply {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index b89eb27..cf9d60f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -19,6 +19,7 @@
 import com.android.keyguard.logging.KeyguardLogger
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.log.core.LogLevel.VERBOSE
 import com.android.systemui.power.domain.interactor.PowerInteractor
@@ -44,6 +45,7 @@
     private val powerInteractor: PowerInteractor,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val keyguardRootViewModel: KeyguardRootViewModel,
+    private val aodBurnInViewModel: AodBurnInViewModel,
     private val shadeInteractor: ShadeInteractor,
     private val keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
 ) {
@@ -132,7 +134,7 @@
         }
 
         scope.launch {
-            keyguardRootViewModel.burnInModel.debounce(20L).collect {
+            aodBurnInViewModel.movement.debounce(20L).collect {
                 logger.log(TAG, VERBOSE, "BurnInModel (debounced)", it)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index ba9f018..5f76f64 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.shared.model.ClockSize
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -58,6 +59,7 @@
         keyguardClockInteractor: KeyguardClockInteractor,
         blueprintInteractor: KeyguardBlueprintInteractor,
         rootViewModel: KeyguardRootViewModel,
+        aodBurnInViewModel: AodBurnInViewModel,
     ): DisposableHandle {
         val disposables = DisposableHandles()
         disposables +=
@@ -78,7 +80,7 @@
                             updateBurnInLayer(
                                 keyguardRootView,
                                 viewModel,
-                                viewModel.clockSize.value
+                                viewModel.clockSize.value,
                             )
                             applyConstraints(clockSection, keyguardRootView, true)
                         }
@@ -114,7 +116,7 @@
                         if (!MigrateClocksToBlueprint.isEnabled) return@launch
                         combine(
                                 viewModel.hasAodIcons,
-                                rootViewModel.isNotifIconContainerVisible.map { it.value }
+                                rootViewModel.isNotifIconContainerVisible.map { it.value },
                             ) { hasIcon, isVisible ->
                                 hasIcon && isVisible
                             }
@@ -130,13 +132,13 @@
 
                     launch {
                         if (!MigrateClocksToBlueprint.isEnabled) return@launch
-                        rootViewModel.burnInModel.collect { burnInModel ->
+                        aodBurnInViewModel.movement.collect { burnInModel ->
                             viewModel.currentClock.value?.let {
                                 it.largeClock.layout.applyAodBurnIn(
                                     AodClockBurnInModel(
                                         translationX = burnInModel.translationX.toFloat(),
                                         translationY = burnInModel.translationY.toFloat(),
-                                        scale = burnInModel.scale
+                                        scale = burnInModel.scale,
                                     )
                                 )
                             }
@@ -175,7 +177,7 @@
     private fun cleanupClockViews(
         currentClock: ClockController?,
         rootView: ConstraintLayout,
-        burnInLayer: Layer?
+        burnInLayer: Layer?,
     ) {
         if (lastClock == currentClock) {
             return
@@ -192,10 +194,7 @@
     }
 
     @VisibleForTesting
-    fun addClockViews(
-        clockController: ClockController?,
-        rootView: ConstraintLayout,
-    ) {
+    fun addClockViews(clockController: ClockController?, rootView: ConstraintLayout) {
         // We'll collect the same clock when exiting wallpaper picker without changing clock
         // so we need to remove clock views from parent before addView again
         clockController?.let { clock ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index b5f6b41..6569e4c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -261,10 +261,7 @@
                             ->
                             if (biometricMessage?.message != null) {
                                 chipbarCoordinator!!.displayView(
-                                    createChipbarInfo(
-                                        biometricMessage.message,
-                                        R.drawable.ic_lock,
-                                    )
+                                    createChipbarInfo(biometricMessage.message, R.drawable.ic_lock)
                                 )
                             } else {
                                 chipbarCoordinator!!.removeView(ID, "occludingAppMsgNull")
@@ -327,6 +324,9 @@
                                     .getDimensionPixelSize(R.dimen.shelf_appear_translation)
                                     .stateIn(this)
                             viewModel.isNotifIconContainerVisible.collect { isVisible ->
+                                if (isVisible.value) {
+                                    blueprintViewModel.refreshBlueprint()
+                                }
                                 childViews[aodNotificationIconContainerId]
                                     ?.setAodNotifIconContainerIsVisible(
                                         isVisible,
@@ -382,7 +382,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.UNLOCK,
-                                        authInteractionProperties
+                                        authInteractionProperties,
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -398,7 +398,7 @@
                                 if (msdlFeedback()) {
                                     msdlPlayer?.playToken(
                                         MSDLToken.FAILURE,
-                                        authInteractionProperties
+                                        authInteractionProperties,
                                     )
                                 } else {
                                     vibratorHelper.performHapticFeedback(
@@ -425,7 +425,7 @@
                     blueprintViewModel,
                     clockViewModel,
                     childViews,
-                    burnInParams
+                    burnInParams,
                 )
             )
 
@@ -464,11 +464,7 @@
      */
     private fun createChipbarInfo(message: String, @DrawableRes icon: Int): ChipbarInfo {
         return ChipbarInfo(
-            startIcon =
-                TintedIcon(
-                    Icon.Resource(icon, null),
-                    ChipbarInfo.DEFAULT_ICON_TINT,
-                ),
+            startIcon = TintedIcon(Icon.Resource(icon, null), ChipbarInfo.DEFAULT_ICON_TINT),
             text = Text.Loaded(message),
             endItem = null,
             vibrationEffect = null,
@@ -499,7 +495,7 @@
             oldLeft: Int,
             oldTop: Int,
             oldRight: Int,
-            oldBottom: Int
+            oldBottom: Int,
         ) {
             // After layout, ensure the notifications are positioned correctly
             childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
@@ -515,7 +511,7 @@
                 viewModel.onNotificationContainerBoundsChanged(
                     notificationListPlaceholder.top.toFloat(),
                     notificationListPlaceholder.bottom.toFloat(),
-                    animate = shouldAnimate
+                    animate = shouldAnimate,
                 )
             }
 
@@ -531,7 +527,7 @@
                                         Int.MAX_VALUE
                                     } else {
                                         view.getTop()
-                                    }
+                                    },
                                 )
                             }
                         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index be6b0eb..ff84826 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
@@ -49,25 +50,17 @@
 import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 
-internal fun ConstraintSet.setVisibility(
-    views: Iterable<View>,
-    visibility: Int,
-) = views.forEach { view -> this.setVisibility(view.id, visibility) }
+internal fun ConstraintSet.setVisibility(views: Iterable<View>, visibility: Int) =
+    views.forEach { view -> this.setVisibility(view.id, visibility) }
 
-internal fun ConstraintSet.setAlpha(
-    views: Iterable<View>,
-    alpha: Float,
-) = views.forEach { view -> this.setAlpha(view.id, alpha) }
+internal fun ConstraintSet.setAlpha(views: Iterable<View>, alpha: Float) =
+    views.forEach { view -> this.setAlpha(view.id, alpha) }
 
-internal fun ConstraintSet.setScaleX(
-    views: Iterable<View>,
-    alpha: Float,
-) = views.forEach { view -> this.setScaleX(view.id, alpha) }
+internal fun ConstraintSet.setScaleX(views: Iterable<View>, alpha: Float) =
+    views.forEach { view -> this.setScaleX(view.id, alpha) }
 
-internal fun ConstraintSet.setScaleY(
-    views: Iterable<View>,
-    alpha: Float,
-) = views.forEach { view -> this.setScaleY(view.id, alpha) }
+internal fun ConstraintSet.setScaleY(views: Iterable<View>, alpha: Float) =
+    views.forEach { view -> this.setScaleY(view.id, alpha) }
 
 @SysUISingleton
 class ClockSection
@@ -79,6 +72,7 @@
     val smartspaceViewModel: KeyguardSmartspaceViewModel,
     val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
     private val rootViewModel: KeyguardRootViewModel,
+    private val aodBurnInViewModel: AodBurnInViewModel,
 ) : KeyguardSection() {
     private var disposableHandle: DisposableHandle? = null
 
@@ -97,6 +91,7 @@
                 clockInteractor,
                 blueprintInteractor.get(),
                 rootViewModel,
+                aodBurnInViewModel,
             )
     }
 
@@ -120,7 +115,7 @@
 
     private fun buildConstraints(
         clock: ClockController,
-        constraintSet: ConstraintSet
+        constraintSet: ConstraintSet,
     ): ConstraintSet {
         // Add constraint between rootView and clockContainer
         applyDefaultConstraints(constraintSet)
@@ -136,8 +131,8 @@
             if (!keyguardClockViewModel.isLargeClockVisible.value) {
                 connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
             } else {
-                setScaleX(getTargetClockFace(clock).views, rootViewModel.burnInModel.value.scale)
-                setScaleY(getTargetClockFace(clock).views, rootViewModel.burnInModel.value.scale)
+                setScaleX(getTargetClockFace(clock).views, aodBurnInViewModel.movement.value.scale)
+                setScaleY(getTargetClockFace(clock).views, aodBurnInViewModel.movement.value.scale)
             }
         }
     }
@@ -156,7 +151,7 @@
                 R.id.weather_clock_bc_smartspace_bottom,
                 Barrier.BOTTOM,
                 getDimen(ENHANCED_SMARTSPACE_HEIGHT),
-                (custR.id.weather_clock_time)
+                (custR.id.weather_clock_time),
             )
             if (
                 rootViewModel.isNotifIconContainerVisible.value.value &&
@@ -168,15 +163,15 @@
                     0,
                     *intArrayOf(
                         R.id.aod_notification_icon_container,
-                        R.id.weather_clock_bc_smartspace_bottom
-                    )
+                        R.id.weather_clock_bc_smartspace_bottom,
+                    ),
                 )
             } else {
                 createBarrier(
                     R.id.weather_clock_date_and_icons_barrier_bottom,
                     Barrier.BOTTOM,
                     0,
-                    *intArrayOf(R.id.weather_clock_bc_smartspace_bottom)
+                    *intArrayOf(R.id.weather_clock_bc_smartspace_bottom),
                 )
             }
         }
@@ -204,7 +199,7 @@
             constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
             constrainHeight(
                 R.id.lockscreen_clock_view,
-                context.resources.getDimensionPixelSize(custR.dimen.small_clock_height)
+                context.resources.getDimensionPixelSize(custR.dimen.small_clock_height),
             )
             connect(
                 R.id.lockscreen_clock_view,
@@ -212,7 +207,7 @@
                 PARENT_ID,
                 START,
                 context.resources.getDimensionPixelSize(custR.dimen.clock_padding_start) +
-                    context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+                    context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal),
             )
             val smallClockTopMargin = keyguardClockViewModel.getSmallClockTopMargin()
             create(R.id.small_clock_guideline_top, ConstraintSet.HORIZONTAL_GUIDELINE)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index 62b4782..998c1c8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.app.animation.Interpolators
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -34,13 +35,17 @@
 import com.android.systemui.res.R
 import javax.inject.Inject
 import kotlin.math.max
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
 
 /**
  * Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD
@@ -50,6 +55,7 @@
 class AodBurnInViewModel
 @Inject
 constructor(
+    @Application private val applicationScope: CoroutineScope,
     private val burnInInteractor: BurnInInteractor,
     private val configurationInteractor: ConfigurationInteractor,
     private val keyguardInteractor: KeyguardInteractor,
@@ -61,91 +67,101 @@
     private val keyguardClockViewModel: KeyguardClockViewModel,
 ) {
     private val TAG = "AodBurnInViewModel"
+    private val burnInParams = MutableStateFlow(BurnInParameters())
 
-    /** All burn-in movement: x,y,scale, to shift items and prevent burn-in */
-    fun movement(
-        burnInParams: BurnInParameters,
-    ): Flow<BurnInModel> {
-        val params =
-            if (burnInParams.minViewY < burnInParams.topInset) {
+    fun updateBurnInParams(params: BurnInParameters) {
+        burnInParams.value =
+            if (params.minViewY < params.topInset) {
                 // minViewY should never be below the inset. Correct it if needed
-                Log.w(TAG, "minViewY is below topInset: $burnInParams")
-                burnInParams.copy(minViewY = burnInParams.topInset)
+                Log.w(TAG, "minViewY is below topInset: $params")
+                params.copy(minViewY = params.topInset)
             } else {
-                burnInParams
+                params
             }
-        return configurationInteractor
-            .dimensionPixelSize(
-                setOf(
-                    R.dimen.keyguard_enter_from_top_translation_y,
-                    R.dimen.keyguard_enter_from_side_translation_x,
-                )
-            )
-            .flatMapLatest { dimens ->
-                combine(
-                    keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
-                    burnIn(params).onStart { emit(BurnInModel()) },
-                    goneToAodTransitionViewModel
-                        .enterFromTopTranslationY(
-                            dimens[R.dimen.keyguard_enter_from_top_translation_y]!!
-                        )
-                        .onStart { emit(StateToValue()) },
-                    goneToAodTransitionViewModel
-                        .enterFromSideTranslationX(
-                            dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
-                        )
-                        .onStart { emit(StateToValue()) },
-                    lockscreenToAodTransitionViewModel
-                        .enterFromSideTranslationX(
-                            dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
-                        )
-                        .onStart { emit(StateToValue()) },
-                    occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
-                        emit(0f)
-                    },
-                    aodToLockscreenTransitionViewModel.translationY(params.translationY).onStart {
-                        emit(StateToValue())
-                    },
-                ) { flows ->
-                    val keyguardTranslationY = flows[0] as Float
-                    val burnInModel = flows[1] as BurnInModel
-                    val goneToAodTranslationY = flows[2] as StateToValue
-                    val goneToAodTranslationX = flows[3] as StateToValue
-                    val lockscreenToAodTranslationX = flows[4] as StateToValue
-                    val occludedToLockscreen = flows[5] as Float
-                    val aodToLockscreen = flows[6] as StateToValue
-
-                    val translationY =
-                        if (aodToLockscreen.transitionState.isTransitioning()) {
-                            aodToLockscreen.value ?: 0f
-                        } else if (goneToAodTranslationY.transitionState.isTransitioning()) {
-                            (goneToAodTranslationY.value ?: 0f) + burnInModel.translationY
-                        } else {
-                            burnInModel.translationY + occludedToLockscreen + keyguardTranslationY
-                        }
-                    val translationX =
-                        burnInModel.translationX +
-                            (goneToAodTranslationX.value ?: 0f) +
-                            (lockscreenToAodTranslationX.value ?: 0f)
-                    burnInModel.copy(
-                        translationX = translationX.toInt(),
-                        translationY = translationY.toInt(),
-                    )
-                }
-            }
-            .distinctUntilChanged()
     }
 
-    private fun burnIn(
-        params: BurnInParameters,
-    ): Flow<BurnInModel> {
+    /** All burn-in movement: x,y,scale, to shift items and prevent burn-in */
+    val movement: StateFlow<BurnInModel> =
+        burnInParams
+            .flatMapLatest { params ->
+                configurationInteractor
+                    .dimensionPixelSize(
+                        setOf(
+                            R.dimen.keyguard_enter_from_top_translation_y,
+                            R.dimen.keyguard_enter_from_side_translation_x,
+                        )
+                    )
+                    .flatMapLatest { dimens ->
+                        combine(
+                            keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
+                            burnIn(params).onStart { emit(BurnInModel()) },
+                            goneToAodTransitionViewModel
+                                .enterFromTopTranslationY(
+                                    dimens[R.dimen.keyguard_enter_from_top_translation_y]!!
+                                )
+                                .onStart { emit(StateToValue()) },
+                            goneToAodTransitionViewModel
+                                .enterFromSideTranslationX(
+                                    dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
+                                )
+                                .onStart { emit(StateToValue()) },
+                            lockscreenToAodTransitionViewModel
+                                .enterFromSideTranslationX(
+                                    dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
+                                )
+                                .onStart { emit(StateToValue()) },
+                            occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
+                                emit(0f)
+                            },
+                            aodToLockscreenTransitionViewModel
+                                .translationY(params.translationY)
+                                .onStart { emit(StateToValue()) },
+                        ) { flows ->
+                            val keyguardTranslationY = flows[0] as Float
+                            val burnInModel = flows[1] as BurnInModel
+                            val goneToAodTranslationY = flows[2] as StateToValue
+                            val goneToAodTranslationX = flows[3] as StateToValue
+                            val lockscreenToAodTranslationX = flows[4] as StateToValue
+                            val occludedToLockscreen = flows[5] as Float
+                            val aodToLockscreen = flows[6] as StateToValue
+
+                            val translationY =
+                                if (aodToLockscreen.transitionState.isTransitioning()) {
+                                    aodToLockscreen.value ?: 0f
+                                } else if (
+                                    goneToAodTranslationY.transitionState.isTransitioning()
+                                ) {
+                                    (goneToAodTranslationY.value ?: 0f) + burnInModel.translationY
+                                } else {
+                                    burnInModel.translationY +
+                                        occludedToLockscreen +
+                                        keyguardTranslationY
+                                }
+                            val translationX =
+                                burnInModel.translationX +
+                                    (goneToAodTranslationX.value ?: 0f) +
+                                    (lockscreenToAodTranslationX.value ?: 0f)
+                            burnInModel.copy(
+                                translationX = translationX.toInt(),
+                                translationY = translationY.toInt(),
+                            )
+                        }
+                    }
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = BurnInModel(),
+            )
+
+    private fun burnIn(params: BurnInParameters): Flow<BurnInModel> {
         return combine(
             keyguardTransitionInteractor.transitionValue(KeyguardState.AOD).map {
                 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it)
             },
             burnInInteractor.burnIn(
                 xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
-                yDimenResourceId = R.dimen.burn_in_prevention_offset_y
+                yDimenResourceId = R.dimen.burn_in_prevention_offset_y,
             ),
         ) { interpolated, burnIn ->
             val useAltAod =
@@ -168,7 +184,7 @@
                 translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(),
                 translationY = translationY,
                 scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated),
-                scaleClockOnly = useScaleOnly
+                scaleClockOnly = useScaleOnly,
             )
         }
     }
@@ -181,7 +197,7 @@
     /** The min y-value of the visible elements on lockscreen */
     val minViewY: Int = Int.MAX_VALUE,
     /** The current y translation of the view */
-    val translationY: () -> Float? = { null }
+    val translationY: () -> Float? = { null },
 )
 
 /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index c6efcfa..4cf3c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -25,20 +25,18 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
-data class TransitionData(
-    val config: Config,
-    val start: Long = System.currentTimeMillis(),
-)
+data class TransitionData(val config: Config, val start: Long = System.currentTimeMillis())
 
 class KeyguardBlueprintViewModel
 @Inject
 constructor(
     @Main private val handler: Handler,
-    keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+    private val keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
 ) {
     val blueprint = keyguardBlueprintInteractor.blueprint
     val blueprintId = keyguardBlueprintInteractor.blueprintId
@@ -76,6 +74,9 @@
             }
         }
 
+    fun refreshBlueprint(type: Type = Type.NoTransition) =
+        keyguardBlueprintInteractor.refreshBlueprint(type)
+
     fun updateTransitions(data: TransitionData?, mutate: MutableSet<Transition>.() -> Unit) {
         runningTransitions.mutate()
 
@@ -95,7 +96,7 @@
                 Log.w(
                     TAG,
                     "runTransition: skipping ${transition::class.simpleName}: " +
-                        "currentPriority=$currentPriority; config=$config"
+                        "currentPriority=$currentPriority; config=$config",
                 )
             }
             apply()
@@ -106,7 +107,7 @@
             Log.i(
                 TAG,
                 "runTransition: running ${transition::class.simpleName}: " +
-                    "currentPriority=$currentPriority; config=$config"
+                    "currentPriority=$currentPriority; config=$config",
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 38ca888..dc0ce34 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -20,7 +20,6 @@
 import android.graphics.Point
 import android.util.MathUtils
 import android.view.View.VISIBLE
-import com.android.app.tracing.coroutines.launch
 import com.android.systemui.Flags.newAodTransition
 import com.android.systemui.common.shared.model.NotificationContainerBounds
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -29,7 +28,6 @@
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.keyguard.shared.model.BurnInModel
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
@@ -45,6 +43,7 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
@@ -58,12 +57,9 @@
 import kotlin.math.max
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
@@ -86,6 +82,7 @@
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
     notificationShadeWindowModel: NotificationShadeWindowModel,
+    private val aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
     private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
     private val alternateBouncerToGoneTransitionViewModel:
         AlternateBouncerToGoneTransitionViewModel,
@@ -127,10 +124,6 @@
     private val aodAlphaViewModel: AodAlphaViewModel,
     private val shadeInteractor: ShadeInteractor,
 ) {
-    private var burnInJob: Job? = null
-    private val _burnInModel = MutableStateFlow(BurnInModel())
-    val burnInModel = _burnInModel.asStateFlow()
-
     val burnInLayerVisibility: Flow<Int> =
         keyguardTransitionInteractor.startedKeyguardTransitionStep
             .filter { it.to == AOD || it.to == LOCKSCREEN }
@@ -139,7 +132,7 @@
     val goneToAodTransition =
         keyguardTransitionInteractor.transition(
             edge = Edge.create(Scenes.Gone, AOD),
-            edgeWithoutSceneContainer = Edge.create(GONE, AOD)
+            edgeWithoutSceneContainer = Edge.create(GONE, AOD),
         )
 
     private val goneToAodTransitionRunning: Flow<Boolean> =
@@ -192,7 +185,7 @@
                                 /* rangeMax = */ 1f,
                                 /* valueMin = */ 0f,
                                 /* valueMax = */ 0.2f,
-                                /* value = */ max(qsExpansion, shadeExpansion)
+                                /* value = */ max(qsExpansion, shadeExpansion),
                             )
                     emit(alpha)
                 }
@@ -263,7 +256,7 @@
                         primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
                     )
-                    .onStart { emit(1f) }
+                    .onStart { emit(1f) },
             ) { hideKeyguard, alpha ->
                 if (hideKeyguard) {
                     0f
@@ -283,30 +276,24 @@
     /** For elements that appear and move during the animation -> AOD */
     val burnInLayerAlpha: Flow<Float> = aodAlphaViewModel.alpha
 
-    val translationY: Flow<Float> = burnInModel.map { it.translationY.toFloat() }
+    val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() }
 
     val translationX: Flow<StateToValue> =
         merge(
-            burnInModel.map { StateToValue(to = AOD, value = it.translationX.toFloat()) },
+            aodBurnInViewModel.movement.map {
+                StateToValue(to = AOD, value = it.translationX.toFloat())
+            },
             lockscreenToGlanceableHubTransitionViewModel.keyguardTranslationX,
             glanceableHubToLockscreenTransitionViewModel.keyguardTranslationX,
         )
 
     fun updateBurnInParams(params: BurnInParameters) {
-        burnInJob?.cancel()
-
-        burnInJob =
-            applicationScope.launch("$TAG#aodBurnInViewModel") {
-                aodBurnInViewModel.movement(params).collect { _burnInModel.value = it }
-            }
+        aodBurnInViewModel.updateBurnInParams(params)
     }
 
     val scale: Flow<BurnInScaleViewModel> =
-        burnInModel.map {
-            BurnInScaleViewModel(
-                scale = it.scale,
-                scaleClockOnly = it.scaleClockOnly,
-            )
+        aodBurnInViewModel.movement.map {
+            BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly)
         }
 
     /** Is the notification icon container visible? */
@@ -319,11 +306,12 @@
                     .onStart { emit(false) },
                 keyguardTransitionInteractor.isFinishedIn(
                     scene = Scenes.Gone,
-                    stateWithoutSceneContainer = GONE
+                    stateWithoutSceneContainer = GONE,
                 ),
                 deviceEntryInteractor.isBypassEnabled,
                 areNotifsFullyHiddenAnimated(),
                 isPulseExpandingAnimated(),
+                aodNotificationIconViewModel.icons.map { it.visibleIcons.isNotEmpty() },
             ) { flows ->
                 val goneToAodTransitionRunning = flows[0] as Boolean
                 val isOnLockscreen = flows[1] as Boolean
@@ -331,6 +319,7 @@
                 val isBypassEnabled = flows[3] as Boolean
                 val notifsFullyHidden = flows[4] as AnimatedValue<Boolean>
                 val pulseExpanding = flows[5] as AnimatedValue<Boolean>
+                val hasAodIcons = flows[6] as Boolean
 
                 when {
                     // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off
@@ -342,9 +331,10 @@
                     else ->
                         zip(notifsFullyHidden, pulseExpanding) {
                             areNotifsFullyHidden,
-                            isPulseExpanding,
-                            ->
+                            isPulseExpanding ->
                             when {
+                                // If there are no notification icons to show, then it can be hidden
+                                !hasAodIcons -> false
                                 // If we're bypassing, then we're visible
                                 isBypassEnabled -> true
                                 // If we are pulsing (and not bypassing), then we are hidden
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 75e3871..c5909ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -107,8 +107,6 @@
                 }
             }
 
-    // TODO(b/365182034): move to interactor, add as dependency of SideFpsOverlayInteractor when
-    //  rest to unlock feature is implemented
     val isVisible: Flow<Boolean> = _visible.asStateFlow()
 
     val progress: Flow<Float> = _progress.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
index 8e53949..649f8db 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java
@@ -50,8 +50,12 @@
 import com.android.settingslib.RestrictedLockUtils;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.systemui.Flags;
+import com.android.systemui.brightness.shared.model.BrightnessLog;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.log.core.LogLevel;
+import com.android.systemui.log.core.LogMessage;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.settings.SecureSettings;
@@ -60,6 +64,8 @@
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
+import kotlin.Unit;
+
 import java.util.concurrent.Executor;
 
 public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController {
@@ -88,6 +94,7 @@
     private final Executor mMainExecutor;
     private final Handler mBackgroundHandler;
     private final BrightnessObserver mBrightnessObserver;
+    private final LogBuffer mLogBuffer;
 
     private final DisplayTracker.Callback mBrightnessListener = new DisplayTracker.Callback() {
         @Override
@@ -308,6 +315,7 @@
             DisplayTracker displayTracker,
             DisplayManager displayManager,
             SecureSettings secureSettings,
+            @BrightnessLog LogBuffer logBuffer,
             @Nullable IVrManager iVrManager,
             @Main Executor mainExecutor,
             @Main Looper mainLooper,
@@ -323,6 +331,7 @@
         mDisplayId = mContext.getDisplayId();
         mDisplayManager = displayManager;
         mVrManager = iVrManager;
+        mLogBuffer = logBuffer;
 
         mMainHandler = new Handler(mainLooper, mHandlerCallback);
         mBrightnessObserver = new BrightnessObserver(mMainHandler);
@@ -342,6 +351,7 @@
 
     @Override
     public void onChanged(boolean tracking, int value, boolean stopTracking) {
+        boolean starting = !mTrackingTouch && tracking;
         mTrackingTouch = tracking;
         if (mExternalChange) return;
 
@@ -369,9 +379,13 @@
 
         }
         setBrightness(valFloat);
+        if (starting) {
+            logBrightnessChange(mDisplayId, valFloat, true);
+        }
         if (!tracking) {
             AsyncTask.execute(new Runnable() {
                     public void run() {
+                        logBrightnessChange(mDisplayId, valFloat, false);
                         mDisplayManager.setBrightness(mDisplayId, valFloat);
                     }
                 });
@@ -474,4 +488,20 @@
         /** Create a {@link BrightnessController} */
         BrightnessController create(ToggleSlider toggleSlider);
     }
+
+    private void logBrightnessChange(int display, float value, boolean starting) {
+        mLogBuffer.log(
+                TAG,
+                LogLevel.DEBUG,
+                (LogMessage message) -> {
+                    message.setInt1(display);
+                    message.setDouble1(value);
+                    message.setBool1(starting);
+                    return Unit.INSTANCE;
+                },
+                (LogMessage message) -> "%s brightness set in display %d to %.3f".formatted(
+                        message.getBool1() ? "Starting" : "Finishing", message.getInt1(),
+                        message.getDouble1())
+        );
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 2e67277..52cb8d6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -289,6 +289,13 @@
         )
     }
 
+    private fun resetTouchMonitor() {
+        touchMonitor?.apply {
+            destroy()
+            touchMonitor = null
+        }
+    }
+
     /** Override for testing. */
     @VisibleForTesting
     internal fun initView(containerView: View): View {
@@ -297,12 +304,13 @@
             throw RuntimeException("Communal view has already been initialized")
         }
 
-        if (touchMonitor == null) {
-            touchMonitor =
-                ambientTouchComponentFactory.create(this, HashSet(), TAG).getTouchMonitor().apply {
-                    init()
-                }
-        }
+        resetTouchMonitor()
+
+        touchMonitor =
+            ambientTouchComponentFactory.create(this, HashSet(), TAG).getTouchMonitor().apply {
+                init()
+            }
+
         lifecycleRegistry.addObserver(touchLifecycleLogger)
         lifecycleRegistry.currentState = Lifecycle.State.CREATED
 
@@ -475,6 +483,8 @@
 
         lifecycleRegistry.removeObserver(touchLifecycleLogger)
 
+        resetTouchMonitor()
+
         logger.d("Hub container disposed")
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 9b96931..6907eef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -106,7 +106,8 @@
     @VisibleForTesting
     public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
             = new ArraySet<>();
-    private boolean mIsExpanded;
+    private boolean mIsShadeOrQsExpanded;
+    private boolean mIsQsExpanded;
     private int mStatusBarState;
     private AnimationStateHandler mAnimationStateHandler;
 
@@ -178,6 +179,10 @@
         });
         javaAdapter.alwaysCollectFlow(shadeInteractor.isAnyExpanded(),
                     this::onShadeOrQsExpanded);
+        if (SceneContainerFlag.isEnabled()) {
+            javaAdapter.alwaysCollectFlow(shadeInteractor.isQsExpanded(),
+                    this::onQsExpanded);
+        }
         if (NotificationThrottleHun.isEnabled()) {
             mVisualStabilityProvider.addPersistentReorderingBannedListener(
                     mOnReorderingBannedListener);
@@ -287,14 +292,19 @@
     }
 
     private void onShadeOrQsExpanded(Boolean isExpanded) {
-        if (isExpanded != mIsExpanded) {
-            mIsExpanded = isExpanded;
+        if (isExpanded != mIsShadeOrQsExpanded) {
+            mIsShadeOrQsExpanded = isExpanded;
             if (!SceneContainerFlag.isEnabled() && isExpanded) {
                 mHeadsUpAnimatingAway.setValue(false);
             }
         }
     }
 
+    private void onQsExpanded(Boolean isQsExpanded) {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+        if (isQsExpanded != mIsQsExpanded) mIsQsExpanded = isQsExpanded;
+    }
+
     /**
      * Set that we are exiting the headsUp pinned mode, but some notifications might still be
      * animating out. This is used to keep the touchable regions in a reasonable state.
@@ -490,7 +500,10 @@
 
     @Override
     protected boolean shouldHeadsUpBecomePinned(NotificationEntry entry) {
-        boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsExpanded;
+        boolean pin = mStatusBarState == StatusBarState.SHADE && !mIsShadeOrQsExpanded;
+        if (SceneContainerFlag.isEnabled()) {
+            pin |= mIsQsExpanded;
+        }
         if (mBypassController.getBypassEnabled()) {
             pin |= mStatusBarState == StatusBarState.KEYGUARD;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 48796d8..b108388 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -634,8 +634,10 @@
     }
 
     private View inflateDivider() {
-        return LayoutInflater.from(mContext).inflate(
+        View divider = LayoutInflater.from(mContext).inflate(
                 R.layout.notification_children_divider, this, false);
+        divider.setAlpha(0f);
+        return divider;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index e7c67f9..3c6962a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1260,6 +1260,7 @@
     @Override
     public void setHeadsUpBottom(float headsUpBottom) {
         mAmbientState.setHeadsUpBottom(headsUpBottom);
+        mStateAnimator.setHeadsUpAppearHeightBottom(Math.round(headsUpBottom));
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 5ae5a32..b22143f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -16,16 +16,12 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
-import android.view.View
-import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.common.ui.view.onApplyWindowInsets
 import com.android.systemui.common.ui.view.onLayoutChanged
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -39,9 +35,6 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 
 /** Binds the shared notification container to its view-model. */
@@ -85,7 +78,6 @@
                 }
             }
 
-        val burnInParams = MutableStateFlow(BurnInParameters())
         val viewState = ViewStateAccessor(alpha = { controller.getAlpha() })
 
         /*
@@ -140,9 +132,7 @@
 
                     if (!SceneContainerFlag.isEnabled) {
                         launch {
-                            burnInParams
-                                .flatMapLatest { params -> viewModel.translationY(params) }
-                                .collect { y -> controller.setTranslationY(y) }
+                            viewModel.translationY.collect { y -> controller.setTranslationY(y) }
                         }
                     }
 
@@ -178,16 +168,6 @@
 
         controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
         disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
-
-        disposables +=
-            view.onApplyWindowInsets { _: View, insets: WindowInsets ->
-                val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
-                burnInParams.update { current ->
-                    current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
-                }
-                insets
-            }
-
         disposables += view.onLayoutChanged { viewModel.notificationStackChanged() }
 
         return disposables
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 8ca26be..57be629 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -44,7 +44,6 @@
 import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel
-import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
 import com.android.systemui.keyguard.ui.viewmodel.DozingToGlanceableHubTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel
 import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel
@@ -536,20 +535,18 @@
      * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
      * translated as the keyguard fades out.
      */
-    fun translationY(params: BurnInParameters): Flow<Float> {
-        // with SceneContainer, x translation is handled by views, y is handled by compose
-        SceneContainerFlag.assertInLegacyMode()
-        return combine(
-                aodBurnInViewModel
-                    .movement(params)
-                    .map { it.translationY.toFloat() }
-                    .onStart { emit(0f) },
+    val translationY: Flow<Float> =
+        combine(
+                aodBurnInViewModel.movement.map { it.translationY.toFloat() }.onStart { emit(0f) },
                 isOnLockscreenWithoutShade,
                 merge(
                     keyguardInteractor.keyguardTranslationY,
                     occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
                 ),
             ) { burnInY, isOnLockscreenWithoutShade, translationY ->
+                // with SceneContainer, x translation is handled by views, y is handled by compose
+                SceneContainerFlag.assertInLegacyMode()
+
                 if (isOnLockscreenWithoutShade) {
                     burnInY + translationY
                 } else {
@@ -557,7 +554,6 @@
                 }
             }
             .dumpWhileCollecting("translationY")
-    }
 
     /** Horizontal translation to apply to the container. */
     val translationX: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
index fa9c6b2..1a1a592e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt
@@ -32,13 +32,14 @@
  * A centralized class maintaining the state of the status bar window.
  *
  * @deprecated use
- *   [com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepository] instead.
+ *   [com.android.systemui.statusbar.window.data.repository.StatusBarWindowStateRepositoryStore.defaultDisplay]
+ *   repo instead.
  *
  * Classes that want to get updates about the status bar window state should subscribe to this class
  * via [addListener] and should NOT add their own callback on [CommandQueue].
  */
 @SysUISingleton
-@Deprecated("Use StatusBarWindowRepository instead")
+@Deprecated("Use StatusBarWindowStateRepositoryStore.defaultDisplay instead")
 class StatusBarWindowStateController
 @Inject
 constructor(@DisplayId private val thisDisplayId: Int, commandQueue: CommandQueue) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
index 678576d..bef8c84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepository.kt
@@ -22,13 +22,13 @@
 import android.app.StatusBarManager.WINDOW_STATE_SHOWING
 import android.app.StatusBarManager.WINDOW_STATUS_BAR
 import android.app.StatusBarManager.WindowVisibleState
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.DisplayId
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.SharingStarted
@@ -40,16 +40,23 @@
  *
  * Classes that want to get updates about the status bar window state should subscribe to
  * [windowState] and should NOT add their own callback on [CommandQueue].
+ *
+ * Each concrete implementation of this class will be for a specific display ID. Use
+ * [StatusBarWindowStateRepositoryStore] to fetch a concrete implementation for a certain display.
  */
-@SysUISingleton
-class StatusBarWindowStateRepository
-@Inject
+interface StatusBarWindowStatePerDisplayRepository {
+    /** Emits the current window state for the status bar on this specific display. */
+    val windowState: StateFlow<StatusBarWindowState>
+}
+
+class StatusBarWindowStatePerDisplayRepositoryImpl
+@AssistedInject
 constructor(
+    @Assisted private val thisDisplayId: Int,
     private val commandQueue: CommandQueue,
-    @DisplayId private val thisDisplayId: Int,
     @Application private val scope: CoroutineScope,
-) {
-    val windowState: StateFlow<StatusBarWindowState> =
+) : StatusBarWindowStatePerDisplayRepository {
+    override val windowState: StateFlow<StatusBarWindowState> =
         conflatedCallbackFlow {
                 val callback =
                     object : CommandQueue.Callbacks {
@@ -84,3 +91,8 @@
         }
     }
 }
+
+@AssistedFactory
+interface StatusBarWindowStatePerDisplayRepositoryFactory {
+    fun create(@Assisted displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt
new file mode 100644
index 0000000..0e33326
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStore.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+/**
+ * Singleton class to create instances of [StatusBarWindowStatePerDisplayRepository] for a specific
+ * display.
+ *
+ * Repository instances for a specific display should be cached so that if multiple classes request
+ * a repository for the same display ID, we only create the repository once.
+ */
+interface StatusBarWindowStateRepositoryStore {
+    val defaultDisplay: StatusBarWindowStatePerDisplayRepository
+
+    fun forDisplay(displayId: Int): StatusBarWindowStatePerDisplayRepository
+}
+
+@SysUISingleton
+class StatusBarWindowStateRepositoryStoreImpl
+@Inject
+constructor(
+    @DisplayId private val displayId: Int,
+    private val factory: StatusBarWindowStatePerDisplayRepositoryFactory,
+) : StatusBarWindowStateRepositoryStore {
+    // Use WeakReferences to store the repositories so that the repositories are kept around so long
+    // as some UI holds a reference to them, but the repositories are cleaned up once no UI is using
+    // them anymore.
+    // See Change-Id Ib490062208506d646add2fe7e5e5d4df5fb3e66e for similar behavior in
+    // MobileConnectionsRepositoryImpl.
+    private val repositoryCache =
+        mutableMapOf<Int, WeakReference<StatusBarWindowStatePerDisplayRepository>>()
+
+    override val defaultDisplay = factory.create(displayId)
+
+    override fun forDisplay(displayId: Int): StatusBarWindowStatePerDisplayRepository {
+        synchronized(repositoryCache) {
+            return repositoryCache[displayId]?.get()
+                ?: factory.create(displayId).also { repositoryCache[displayId] = WeakReference(it) }
+        }
+    }
+}
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 4850510..55fd344 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
@@ -54,7 +54,6 @@
 import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
 import com.android.systemui.biometrics.domain.interactor.promptSelectorInteractor
-import com.android.systemui.biometrics.domain.interactor.sideFpsOverlayInteractor
 import com.android.systemui.biometrics.domain.interactor.udfpsOverlayInteractor
 import com.android.systemui.biometrics.extractAuthenticatorTypes
 import com.android.systemui.biometrics.faceSensorPropertiesInternal
@@ -1454,15 +1453,11 @@
     @Test
     fun switch_to_credential_fallback() = runGenericTest {
         val size by collectLastValue(kosmos.promptViewModel.size)
-        val isShowingSfpsIndicator by collectLastValue(kosmos.sideFpsOverlayInteractor.isShowing)
 
         // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test
         kosmos.promptViewModel.onSwitchToCredential()
 
         assertThat(size).isEqualTo(PromptSize.LARGE)
-        if (testCase.modalities.hasSfps) {
-            assertThat(isShowingSfpsIndicator).isFalse()
-        }
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 6e381ca..0b944f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor
 import com.android.systemui.keyguard.shared.model.ClockSize
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardSmartspaceViewModel
@@ -120,6 +121,7 @@
                     keyguardSmartspaceViewModel,
                     { keyguardBlueprintInteractor },
                     keyguardRootViewModel,
+                    aodBurnInViewModel,
                 )
         }
     }
@@ -313,7 +315,7 @@
             referencedIds.contentEquals(
                 intArrayOf(
                     com.android.systemui.shared.R.id.bc_smartspace_view,
-                    R.id.aod_notification_icon_container
+                    R.id.aod_notification_icon_container,
                 )
             )
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
index 98260d8..41e2467 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.LogBuffer
 import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -51,6 +52,7 @@
     @Mock private lateinit var displayTracker: DisplayTracker
     @Mock private lateinit var displayManager: DisplayManager
     @Mock private lateinit var iVrManager: IVrManager
+    @Mock private lateinit var logger: LogBuffer
 
     private lateinit var testableLooper: TestableLooper
 
@@ -69,10 +71,11 @@
                 displayTracker,
                 displayManager,
                 secureSettings,
+                logger,
                 iVrManager,
                 executor,
                 mock(),
-                Handler(testableLooper.looper)
+                Handler(testableLooper.looper),
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index b4a0f23..859f84e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -76,6 +76,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
+import org.mockito.kotlin.clearInvocations
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
@@ -717,6 +718,15 @@
             }
         }
 
+    @Test
+    fun disposeView_destroysTouchMonitor() {
+        clearInvocations(touchMonitor)
+
+        underTest.disposeView()
+
+        verify(touchMonitor).destroy()
+    }
+
     private fun initAndAttachContainerView() {
         val mockInsets =
             mock<WindowInsets> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 7ddf7a3..93ba8e1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -35,7 +35,8 @@
 import com.android.systemui.plugins.Plugin;
 import com.android.systemui.plugins.PluginLifecycleManager;
 import com.android.systemui.plugins.PluginListener;
-import com.android.systemui.plugins.annotations.ProvidesInterface;
+import com.android.systemui.plugins.PluginWrapper;
+import com.android.systemui.plugins.TestPlugin;
 import com.android.systemui.plugins.annotations.Requires;
 
 import org.junit.Before;
@@ -60,7 +61,7 @@
 
     private FakeListener mPluginListener;
     private VersionInfo mVersionInfo;
-    private VersionInfo.InvalidVersionException mVersionException;
+    private boolean mVersionCheckResult = true;
     private PluginInstance.VersionChecker mVersionChecker;
 
     private RefCounter mCounter;
@@ -83,14 +84,16 @@
         mVersionInfo = new VersionInfo();
         mVersionChecker = new PluginInstance.VersionChecker() {
             @Override
-            public <T extends Plugin> VersionInfo checkVersion(
+            public <T extends Plugin> boolean checkVersion(
                     Class<T> instanceClass,
                     Class<T> pluginClass,
                     Plugin plugin
             ) {
-                if (mVersionException != null) {
-                    throw mVersionException;
-                }
+                return mVersionCheckResult;
+            }
+
+            @Override
+            public <T extends Plugin> VersionInfo getVersionInfo(Class<T> instanceClass) {
                 return mVersionInfo;
             }
         };
@@ -117,21 +120,29 @@
     }
 
     @Test
-    public void testCorrectVersion() {
-        assertNotNull(mPluginInstance);
+    public void testCorrectVersion_onCreateBuildsPlugin() {
+        mVersionCheckResult = true;
+        assertFalse(mPluginInstance.hasError());
+
+        mPluginInstance.onCreate();
+        assertFalse(mPluginInstance.hasError());
+        assertNotNull(mPluginInstance.getPlugin());
     }
 
-    @Test(expected = VersionInfo.InvalidVersionException.class)
-    public void testIncorrectVersion() throws Exception {
+    @Test
+    public void testIncorrectVersion_destroysPluginInstance() throws Exception {
         ComponentName wrongVersionTestPluginComponentName =
                 new ComponentName(PRIVILEGED_PACKAGE, TestPlugin.class.getName());
 
-        mVersionException = new VersionInfo.InvalidVersionException("test", true);
+        mVersionCheckResult = false;
+        assertFalse(mPluginInstance.hasError());
 
         mPluginInstanceFactory.create(
                 mContext, mAppInfo, wrongVersionTestPluginComponentName,
                 TestPlugin.class, mPluginListener);
         mPluginInstance.onCreate();
+        assertTrue(mPluginInstance.hasError());
+        assertNull(mPluginInstance.getPlugin());
     }
 
     @Test
@@ -139,7 +150,7 @@
         mPluginInstance.onCreate();
         assertEquals(1, mPluginListener.mAttachedCount);
         assertEquals(1, mPluginListener.mLoadCount);
-        assertEquals(mPlugin.get(), mPluginInstance.getPlugin());
+        assertEquals(mPlugin.get(), unwrap(mPluginInstance.getPlugin()));
         assertInstances(1, 1);
     }
 
@@ -176,6 +187,17 @@
     }
 
     @Test
+    public void testLinkageError_caughtAndPluginDestroyed() {
+        mPluginInstance.onCreate();
+        assertFalse(mPluginInstance.hasError());
+
+        Object result = mPluginInstance.getPlugin().methodThrowsError();
+        assertNotNull(result);  // Wrapper function should return non-null;
+        assertTrue(mPluginInstance.hasError());
+        assertNull(mPluginInstance.getPlugin());
+    }
+
+    @Test
     public void testLoadUnloadSimultaneous_HoldsUnload() throws Throwable {
         final Semaphore loadLock = new Semaphore(1);
         final Semaphore unloadLock = new Semaphore(1);
@@ -232,6 +254,13 @@
         assertNull(mPluginInstance.getPlugin());
     }
 
+    private static <T> T unwrap(T plugin) {
+        if (plugin instanceof PluginWrapper) {
+            return ((PluginWrapper<T>) plugin).getPlugin();
+        }
+        return plugin;
+    }
+
     private boolean getLock(Semaphore lock, long millis) {
         try {
             return lock.tryAcquire(millis, TimeUnit.MILLISECONDS);
@@ -243,14 +272,6 @@
         }
     }
 
-    // This target class doesn't matter, it just needs to have a Requires to hit the flow where
-    // the mock version info is called.
-    @ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
-    public interface TestPlugin extends Plugin {
-        int VERSION = 1;
-        String ACTION = "testAction";
-    }
-
     private void assertInstances(int allocated, int created) {
         // If there are more than the expected number of allocated instances, then we run the
         // garbage collector to finalize and deallocate any outstanding non-referenced instances.
@@ -300,6 +321,11 @@
         public void onDestroy() {
             mCounter.mCreatedInstances.getAndDecrement();
         }
+
+        @Override
+        public Object methodThrowsError() {
+            throw new LinkageError();
+        }
     }
 
     public class FakeListener implements PluginListener<TestPlugin> {
@@ -337,7 +363,7 @@
             mLoadCount++;
             TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get();
             if (expectedPlugin != null) {
-                assertEquals(expectedPlugin, plugin);
+                assertEquals(expectedPlugin, unwrap(plugin));
             }
             Context expectedContext = PluginInstanceTest.this.mPluginContext.get();
             if (expectedContext != null) {
@@ -357,7 +383,7 @@
             mUnloadCount++;
             TestPlugin expectedPlugin = PluginInstanceTest.this.mPlugin.get();
             if (expectedPlugin != null) {
-                assertEquals(expectedPlugin, plugin);
+                assertEquals(expectedPlugin, unwrap(plugin));
             }
             assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
             if (mOnUnload != null) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
index 38e04bb..0c27e58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStatePerDisplayRepositoryTest.kt
@@ -39,12 +39,16 @@
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
-class StatusBarWindowStateRepositoryTest : SysuiTestCase() {
+class StatusBarWindowStatePerDisplayRepositoryTest : SysuiTestCase() {
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val commandQueue = kosmos.commandQueue
     private val underTest =
-        StatusBarWindowStateRepository(commandQueue, DISPLAY_ID, testScope.backgroundScope)
+        StatusBarWindowStatePerDisplayRepositoryImpl(
+            DISPLAY_ID,
+            commandQueue,
+            testScope.backgroundScope,
+        )
 
     private val callback: CommandQueue.Callbacks
         get() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt
new file mode 100644
index 0000000..b6a3f36
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreTest.kt
@@ -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.systemui.statusbar.window.data.repository
+
+import android.app.StatusBarManager.WINDOW_STATE_HIDDEN
+import android.app.StatusBarManager.WINDOW_STATE_SHOWING
+import android.app.StatusBarManager.WINDOW_STATUS_BAR
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.commandQueue
+import com.android.systemui.statusbar.window.data.model.StatusBarWindowState
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.reset
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class StatusBarWindowStateRepositoryStoreTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val commandQueue = kosmos.commandQueue
+    private val defaultDisplayId = kosmos.displayTracker.defaultDisplayId
+
+    private val underTest = kosmos.statusBarWindowStateRepositoryStore
+
+    @Test
+    fun defaultDisplay_repoIsForDefaultDisplay() =
+        testScope.runTest {
+            val repo = underTest.defaultDisplay
+            val latest by collectLastValue(repo.windowState)
+
+            testScope.runCurrent()
+            val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+            verify(commandQueue).addCallback(callbackCaptor.capture())
+            val callback = callbackCaptor.firstValue
+
+            // WHEN a default display callback is sent
+            callback.setWindowState(defaultDisplayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+            // THEN its value is used
+            assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+
+            // WHEN a non-default display callback is sent
+            callback.setWindowState(defaultDisplayId + 1, WINDOW_STATUS_BAR, WINDOW_STATE_HIDDEN)
+
+            // THEN its value is NOT used
+            assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+        }
+
+    @Test
+    fun forDisplay_repoIsForSpecifiedDisplay() =
+        testScope.runTest {
+            // The repository store will always create a repository for the default display, which
+            // will always add a callback to commandQueue. Ignore that callback here.
+            testScope.runCurrent()
+            reset(commandQueue)
+
+            val displayId = defaultDisplayId + 15
+            val repo = underTest.forDisplay(displayId)
+            val latest by collectLastValue(repo.windowState)
+
+            testScope.runCurrent()
+            val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+            verify(commandQueue).addCallback(callbackCaptor.capture())
+            val callback = callbackCaptor.firstValue
+
+            // WHEN a default display callback is sent
+            callback.setWindowState(defaultDisplayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+            // THEN its value is NOT used
+            assertThat(latest).isEqualTo(StatusBarWindowState.Hidden)
+
+            // WHEN a callback for this display is sent
+            callback.setWindowState(displayId, WINDOW_STATUS_BAR, WINDOW_STATE_SHOWING)
+
+            // THEN its value is used
+            assertThat(latest).isEqualTo(StatusBarWindowState.Showing)
+        }
+
+    @Test
+    fun forDisplay_reusesRepoForSameDisplayId() =
+        testScope.runTest {
+            // The repository store will always create a repository for the default display, which
+            // will always add a callback to commandQueue. Ignore that callback here.
+            testScope.runCurrent()
+            reset(commandQueue)
+
+            val displayId = defaultDisplayId + 15
+            val firstRepo = underTest.forDisplay(displayId)
+            testScope.runCurrent()
+            val secondRepo = underTest.forDisplay(displayId)
+            testScope.runCurrent()
+
+            assertThat(firstRepo).isEqualTo(secondRepo)
+            // Verify that we only added 1 CommandQueue.Callback because we only created 1 repo.
+            val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>()
+            verify(commandQueue).addCallback(callbackCaptor.capture())
+        }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorKosmos.kt
deleted file mode 100644
index 15c7e25..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorKosmos.kt
+++ /dev/null
@@ -1,32 +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.biometrics.domain.interactor
-
-import com.android.systemui.keyguard.domain.interactor.deviceEntrySideFpsOverlayInteractor
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.Kosmos.Fixture
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-@OptIn(ExperimentalCoroutinesApi::class)
-val Kosmos.sideFpsOverlayInteractor by Fixture {
-    SideFpsOverlayInteractorImpl(
-        biometricStatusInteractor,
-        displayStateInteractor,
-        deviceEntrySideFpsOverlayInteractor,
-        sideFpsSensorInteractor,
-    )
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderKosmos.kt
index 59809e3..79d58a1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderKosmos.kt
@@ -19,19 +19,27 @@
 import android.content.applicationContext
 import android.view.layoutInflater
 import android.view.windowManager
-import com.android.systemui.biometrics.domain.interactor.sideFpsOverlayInteractor
-import com.android.systemui.biometrics.ui.viewmodel.sideFpsOverlayViewModel
+import com.android.systemui.biometrics.domain.interactor.biometricStatusInteractor
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor
+import com.android.systemui.keyguard.domain.interactor.deviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.ui.viewmodel.sideFpsProgressBarViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
+@OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.sideFpsOverlayViewBinder by Fixture {
     SideFpsOverlayViewBinder(
-        applicationCoroutineScope,
-        applicationContext,
+        applicationScope = applicationCoroutineScope,
+        applicationContext = applicationContext,
+        { biometricStatusInteractor },
+        { displayStateInteractor },
+        { deviceEntrySideFpsOverlayInteractor },
         { layoutInflater },
-        { sideFpsOverlayInteractor },
-        { sideFpsOverlayViewModel },
+        { sideFpsProgressBarViewModel },
+        { sideFpsSensorInteractor },
         { windowManager }
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelKosmos.kt
index e10b2dd..de03855 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelKosmos.kt
@@ -27,9 +27,9 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.sideFpsOverlayViewModel by Fixture {
     SideFpsOverlayViewModel(
-        applicationContext,
-        deviceEntrySideFpsOverlayInteractor,
-        displayStateInteractor,
-        sideFpsSensorInteractor,
+        applicationContext = applicationContext,
+        deviceEntrySideFpsOverlayInteractor = deviceEntrySideFpsOverlayInteractor,
+        displayStateInteractor = displayStateInteractor,
+        sfpsSensorInteractor = sideFpsSensorInteractor,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
index fb12897..12d7c49 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardSmartspaceViewModel
@@ -41,6 +42,7 @@
             smartspaceViewModel = keyguardSmartspaceViewModel,
             blueprintInteractor = { keyguardBlueprintInteractor },
             rootViewModel = keyguardRootViewModel,
+            aodBurnInViewModel = aodBurnInViewModel,
         )
     }
 
@@ -95,11 +97,7 @@
     Kosmos.Fixture {
         spy(
             KeyguardBlueprintRepository(
-                blueprints =
-                    setOf(
-                        defaultKeyguardBlueprint,
-                        splitShadeBlueprint,
-                    ),
+                blueprints = setOf(defaultKeyguardBlueprint, splitShadeBlueprint),
                 handler = fakeExecutorHandler,
                 assert = mock(),
             )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
index 6cf668c..c3c2c8c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -24,10 +24,12 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 var Kosmos.aodBurnInViewModel by Fixture {
     AodBurnInViewModel(
+        applicationScope = applicationCoroutineScope,
         burnInInteractor = burnInInteractor,
         configurationInteractor = configurationInteractor,
         keyguardInteractor = keyguardInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 7cf4083..38626a5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerAlwaysOnDisplayViewModel
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.phone.screenOffAnimationController
@@ -40,6 +41,7 @@
         communalInteractor = communalInteractor,
         keyguardTransitionInteractor = keyguardTransitionInteractor,
         notificationsKeyguardInteractor = notificationsKeyguardInteractor,
+        aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel,
         notificationShadeWindowModel = notificationShadeWindowModel,
         alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel,
         alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
new file mode 100644
index 0000000..e2b7f5f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/data/repository/StatusBarWindowStateRepositoryStoreKosmos.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.statusbar.commandQueue
+
+class KosmosStatusBarWindowStatePerDisplayRepositoryFactory(private val kosmos: Kosmos) :
+    StatusBarWindowStatePerDisplayRepositoryFactory {
+    override fun create(displayId: Int): StatusBarWindowStatePerDisplayRepositoryImpl {
+        return StatusBarWindowStatePerDisplayRepositoryImpl(
+            displayId,
+            kosmos.commandQueue,
+            kosmos.applicationCoroutineScope,
+        )
+    }
+}
+
+val Kosmos.statusBarWindowStateRepositoryStore by
+    Kosmos.Fixture {
+        StatusBarWindowStateRepositoryStoreImpl(
+            displayId = displayTracker.defaultDisplayId,
+            factory = KosmosStatusBarWindowStatePerDisplayRepositoryFactory(this),
+        )
+    }
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index d4d188d..86246e2 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -34,15 +34,18 @@
     },
     {
       "name": "CarLibHostUnitTest",
-      "host": true
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
     },
     {
       "name": "CarServiceHostUnitTest",
-      "host": true
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
     },
     {
       "name": "CarSystemUIRavenTests",
-      "host": true
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
     },
     {
       "name": "CtsAccountManagerTestCasesRavenwood",
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index aa57e0b..a19fddd 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -68,11 +68,15 @@
 import com.android.internal.R;
 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.expresslog.Histogram;
 import com.android.server.accessibility.AccessibilityManagerService;
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.gestures.GestureUtils;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * This class handles full screen magnification in response to touch events.
  *
@@ -871,6 +875,15 @@
      */
     class DetectingState implements State, Handler.Callback {
 
+        private static final Histogram HISTOGRAM_FIRST_INTERVAL =
+                new Histogram(
+                        "accessibility.value_full_triple_tap_first_interval",
+                        new Histogram.UniformOptions(25, 0, 250));
+        private static final Histogram HISTOGRAM_SECOND_INTERVAL =
+                new Histogram(
+                        "accessibility.value_full_triple_tap_second_interval",
+                        new Histogram.UniformOptions(25, 0, 250));
+
         private static final int MESSAGE_ON_TRIPLE_TAP_AND_HOLD = 1;
         private static final int MESSAGE_TRANSITION_TO_DELEGATING_STATE = 2;
         private static final int MESSAGE_TRANSITION_TO_PANNINGSCALING_STATE = 3;
@@ -1115,6 +1128,12 @@
             if (multitapTriggered && numTaps > 2) {
                 final boolean enabled = !isActivated();
                 mMagnificationLogger.logMagnificationTripleTap(enabled);
+
+                List<Long> intervals = intervalsOf(mDelayedEventQueue, ACTION_UP);
+                if (intervals.size() >= 2) {
+                    HISTOGRAM_FIRST_INTERVAL.logSample(intervals.get(0));
+                    HISTOGRAM_SECOND_INTERVAL.logSample(intervals.get(1));
+                }
             }
             return multitapTriggered;
         }
@@ -1144,6 +1163,10 @@
             return event != null ? event.getEventTime() : Long.MIN_VALUE;
         }
 
+        public List<Long> intervalsOf(MotionEventInfo info, int eventType) {
+            return MotionEventInfo.intervalsOf(info, eventType);
+        }
+
         public int tapCount() {
             return MotionEventInfo.countOf(mDelayedEventQueue, ACTION_UP);
         }
@@ -1649,7 +1672,7 @@
         return !(Float.isNaN(pointerDownLocation.x) && Float.isNaN(pointerDownLocation.y));
     }
 
-    private static final class MotionEventInfo {
+    public static final class MotionEventInfo {
 
         private static final int MAX_POOL_SIZE = 10;
         private static final Object sLock = new Object();
@@ -1709,6 +1732,14 @@
             }
         }
 
+        public MotionEventInfo getNext() {
+            return mNext;
+        }
+
+        public void setNext(MotionEventInfo info) {
+            mNext = info;
+        }
+
         private void clear() {
             event = recycleAndNullify(event);
             rawEvent = recycleAndNullify(rawEvent);
@@ -1721,6 +1752,23 @@
                     + countOf(info.mNext, eventType);
         }
 
+        static List<Long> intervalsOf(MotionEventInfo info, int eventType) {
+            List<Long> intervals = new ArrayList<>();
+            MotionEventInfo current = info;
+            MotionEventInfo previous = null;
+
+            while (current != null) {
+                if (current.event.getAction() == eventType) {
+                    if (previous != null) {
+                        intervals.add(current.event.getDownTime() - previous.event.getDownTime());
+                    }
+                    previous = current;
+                }
+                current = current.mNext;
+            }
+            return intervals;
+        }
+
         public static String toString(MotionEventInfo info) {
             return info == null
                     ? ""
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 4e36e3f..c6e599e 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -225,6 +225,7 @@
         "updates_flags_lib",
         "com_android_server_accessibility_flags_lib",
         "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
+        "com_android_launcher3_flags_lib",
         "com_android_wm_shell_flags_lib",
         "com.android.server.utils_aconfig-java",
         "service-jobscheduler-deviceidle.flags-aconfig-java",
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 0e19347..210301e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -133,6 +133,7 @@
 import com.android.server.am.nano.VMInfo;
 import com.android.server.compat.PlatformCompat;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.AnrTimer;
 import com.android.server.utils.Slogf;
 
 import dalvik.annotation.optimization.NeverCompile;
@@ -285,6 +286,8 @@
                     return -1;
                 case "trace-ipc":
                     return runTraceIpc(pw);
+                case "trace-timer":
+                    return runTraceTimer(pw);
                 case "profile":
                     return runProfile(pw);
                 case "dumpheap":
@@ -1062,6 +1065,23 @@
         return 0;
     }
 
+    // Update AnrTimer tracing.
+    private int runTraceTimer(PrintWriter pw) throws RemoteException {
+        if (!AnrTimer.traceFeatureEnabled()) return -1;
+
+        // Delegate all argument parsing to the AnrTimer method.
+        try {
+            final String result = AnrTimer.traceTimers(peekRemainingArgs());
+            if (result != null) {
+                pw.println(result);
+            }
+            return 0;
+        } catch (IllegalArgumentException e) {
+            getErrPrintWriter().println("Error: bad trace-timer command: " + e);
+            return -1;
+        }
+    }
+
     // NOTE: current profiles can only be started on default display (even on automotive builds with
     // passenger displays), so there's no need to pass a display-id
     private int runProfile(PrintWriter pw) throws RemoteException {
@@ -4352,6 +4372,7 @@
             pw.println("      start: start tracing IPC transactions.");
             pw.println("      stop: stop tracing IPC transactions and dump the results to file.");
             pw.println("      --dump-file <FILE>: Specify the file the trace should be dumped to.");
+            anrTimerHelp(pw);
             pw.println("  profile start [--user <USER_ID> current]");
             pw.println("          [--clock-type <TYPE>]");
             pw.println("          [" + PROFILER_OUTPUT_VERSION_FLAG + " VERSION]");
@@ -4605,4 +4626,19 @@
             Intent.printIntentArgsHelp(pw, "");
         }
     }
+
+    static void anrTimerHelp(PrintWriter pw) {
+        // Return silently if tracing is not feature-enabled.
+        if (!AnrTimer.traceFeatureEnabled()) return;
+
+        String h = AnrTimer.traceTimers(new String[]{"help"});
+        if (h == null) {
+            return;
+        }
+
+        pw.println("  trace-timer <cmd>");
+        for (String s : h.split("\n")) {
+            pw.println("         " + s);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index f7085b4..15f1085 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -57,6 +57,7 @@
 import android.app.ApplicationThreadConstants;
 import android.app.BackgroundStartPrivileges;
 import android.app.BroadcastOptions;
+import android.app.BroadcastStickyCache;
 import android.app.IApplicationThread;
 import android.app.compat.CompatChanges;
 import android.appwidget.AppWidgetManager;
@@ -685,6 +686,7 @@
             boolean serialized, boolean sticky, int userId) {
         mService.enforceNotIsolatedCaller("broadcastIntent");
 
+        int result;
         synchronized (mService) {
             intent = verifyBroadcastLocked(intent);
 
@@ -706,7 +708,7 @@
 
             final long origId = Binder.clearCallingIdentity();
             try {
-                return broadcastIntentLocked(callerApp,
+                result = broadcastIntentLocked(callerApp,
                         callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
                         intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
                         resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
@@ -717,6 +719,10 @@
                 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
             }
         }
+        if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+            BroadcastStickyCache.incrementVersion(intent.getAction());
+        }
+        return result;
     }
 
     // Not the binder call surface
@@ -727,6 +733,7 @@
             boolean serialized, boolean sticky, int userId,
             BackgroundStartPrivileges backgroundStartPrivileges,
             @Nullable int[] broadcastAllowList) {
+        int result;
         synchronized (mService) {
             intent = verifyBroadcastLocked(intent);
 
@@ -734,7 +741,7 @@
             String[] requiredPermissions = requiredPermission == null ? null
                     : new String[] {requiredPermission};
             try {
-                return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
+                result = broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
                         resultToApp, resultTo, resultCode, resultData, resultExtras,
                         requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
                         uid, realCallingUid, realCallingPid, userId,
@@ -744,6 +751,10 @@
                 Binder.restoreCallingIdentity(origId);
             }
         }
+        if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+            BroadcastStickyCache.incrementVersion(intent.getAction());
+        }
+        return result;
     }
 
     @GuardedBy("mService")
@@ -1442,6 +1453,7 @@
                     list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
                             callingUid, callerAppProcessState, resolvedType));
                 }
+                BroadcastStickyCache.incrementVersion(intent.getAction());
             }
         }
 
@@ -1708,6 +1720,7 @@
             Slog.w(TAG, msg);
             throw new SecurityException(msg);
         }
+        final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
         synchronized (mStickyBroadcasts) {
             ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
             if (stickies != null) {
@@ -1724,12 +1737,16 @@
                     if (list.size() <= 0) {
                         stickies.remove(intent.getAction());
                     }
+                    changedStickyBroadcasts.add(intent.getAction());
                 }
                 if (stickies.size() <= 0) {
                     mStickyBroadcasts.remove(userId);
                 }
             }
         }
+        for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+            BroadcastStickyCache.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+        }
     }
 
     void finishReceiver(IBinder caller, int resultCode, String resultData,
@@ -1892,7 +1909,9 @@
 
     private void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) {
         mService.mProcessList.sendPackageBroadcastLocked(cmd, packages, userId);
-    }private List<ResolveInfo> collectReceiverComponents(
+    }
+
+    private List<ResolveInfo> collectReceiverComponents(
             Intent intent, String resolvedType, int callingUid, int callingPid,
             int[] users, int[] broadcastAllowList) {
         // TODO: come back and remove this assumption to triage all broadcasts
@@ -2108,9 +2127,18 @@
     }
 
     void removeStickyBroadcasts(int userId) {
+        final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
         synchronized (mStickyBroadcasts) {
+            final ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
+                    mStickyBroadcasts.get(userId);
+            if (stickies != null) {
+                changedStickyBroadcasts.addAll(stickies.keySet());
+            }
             mStickyBroadcasts.remove(userId);
         }
+        for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+            BroadcastStickyCache.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+        }
     }
 
     @NeverCompile
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index 1bcf825..2a30ad0 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -19,12 +19,12 @@
 per-file AppFGSTracker.java = file:/ACTIVITY_MANAGER_OWNERS
 per-file FgsTempAllowList.java = file:/ACTIVITY_MANAGER_OWNERS
 per-file HostingRecord.java = file:/ACTIVITY_MANAGER_OWNERS
-
-# Windows & Activities
-ogunwale@google.com
+per-file App*ExitInfo* = file:/ACTIVITY_MANAGER_OWNERS
+per-file appexitinfo.proto = file:/ACTIVITY_MANAGER_OWNERS
+per-file App*StartInfo* = file:/PERFORMANCE_OWNERS
+per-file appstartinfo.proto = file:/PERFORMANCE_OWNERS
 
 # Permissions & Packages
-patb@google.com
 per-file AccessCheckDelegateHelper.java = file:/core/java/android/permission/OWNERS
 
 # Battery Stats
@@ -66,6 +66,6 @@
 narayan@google.com #{LAST_RESORT_SUGGESTION}
 
 # Default
+yamasani@google.com
 hackbod@google.com #{LAST_RESORT_SUGGESTION}
-omakoto@google.com #{LAST_RESORT_SUGGESTION}
-yamasani@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
+omakoto@google.com #{LAST_RESORT_SUGGESTION}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 6af4be5..a6389f7 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -423,7 +423,7 @@
 
     /** Hands the definition of foreground and uid states */
     @GuardedBy("this")
-    public AppOpsUidStateTracker getUidStateTracker() {
+    private AppOpsUidStateTracker getUidStateTracker() {
         if (mUidStateTracker == null) {
             mUidStateTracker = new AppOpsUidStateTrackerImpl(
                     LocalServices.getService(ActivityManagerInternal.class),
@@ -2895,21 +2895,28 @@
                         uidState.uid, getPersistentId(virtualDeviceId), code);
 
                 if (rawUidMode != AppOpsManager.opToDefaultMode(code)) {
-                    return raw ? rawUidMode : uidState.evalMode(code, rawUidMode);
+                    return raw ? rawUidMode :
+                        evaluateForegroundMode(/* uid= */ uid, /* op= */ code,
+                        /* rawUidMode= */ rawUidMode);
                 }
             }
 
             Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false);
             if (op == null) {
-                return AppOpsManager.opToDefaultMode(code);
+                return evaluateForegroundMode(
+                        /* uid= */ uid,
+                        /* op= */ code,
+                        /* rawUidMode= */ AppOpsManager.opToDefaultMode(code));
             }
-            return raw
-                    ? mAppOpsCheckingService.getPackageMode(
-                            op.packageName, op.op, UserHandle.getUserId(op.uid))
-                    : op.uidState.evalMode(
-                            op.op,
-                            mAppOpsCheckingService.getPackageMode(
-                                    op.packageName, op.op, UserHandle.getUserId(op.uid)));
+            var packageMode = mAppOpsCheckingService.getPackageMode(
+                    op.packageName,
+                    op.op,
+                    UserHandle.getUserId(op.uid));
+            return raw ? packageMode :
+                    evaluateForegroundMode(
+                        /* uid= */ uid,
+                        /* op= */op.op,
+                        /* rawUidMode= */ packageMode);
         }
     }
 
@@ -7003,6 +7010,11 @@
                 "Requested persistentId for invalid virtualDeviceId: " + virtualDeviceId);
     }
 
+    @GuardedBy("this")
+    private int evaluateForegroundMode(int uid, int op, int rawUidMode) {
+        return getUidStateTracker().evalMode(uid, op, rawUidMode);
+    }
+
     private final class ClientUserRestrictionState implements DeathRecipient {
         private final IBinder token;
 
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 13e3348b..0fd22c5 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -339,8 +339,8 @@
             Log.v(TAG, "setCommunicationDevice, device: " + device + ", uid: " + uid);
         }
 
-        synchronized (mDeviceStateLock) {
-            if (device == null) {
+        if (device == null) {
+            synchronized (mDeviceStateLock) {
                 CommunicationRouteClient client = getCommunicationRouteClientForUid(uid);
                 if (client == null) {
                     return false;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 101596d..aae7b59 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -261,6 +261,7 @@
                 .setDisplayName(HdmiUtils.getDefaultDeviceName(source))
                 .setDeviceType(deviceTypes.get(0))
                 .setVendorId(Constants.VENDOR_ID_UNKNOWN)
+                .setPortId(mService.getHdmiCecNetwork().physicalAddressToPortId(physicalAddress))
                 .build();
         mService.getHdmiCecNetwork().addCecDevice(newDevice);
     }
@@ -1433,6 +1434,7 @@
     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
         assertRunOnServiceThread();
         mService.unregisterTvInputCallback(mTvInputCallback);
+        mTvInputs.clear();
         // Remove any repeated working actions.
         // HotplugDetectionAction will be reinstated during the wake up process.
         // HdmiControlService.onWakeUp() -> initializeLocalDevices() ->
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index 19c802b..9cfbfa64 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.input.debug;
 
+import android.annotation.AnyThread;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.input.InputManager;
@@ -157,19 +158,28 @@
      * @param touchpadHardwareState the hardware state of a touchpad
      * @param deviceId              the deviceId of the touchpad that is sending the hardware state
      */
+    @AnyThread
     public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState,
                                             int deviceId) {
-        if (mTouchpadDebugView != null) {
-            mTouchpadDebugView.updateHardwareState(touchpadHardwareState, deviceId);
-        }
+        mHandler.post(() -> {
+            if (mTouchpadDebugView != null) {
+                mTouchpadDebugView.post(
+                        () -> mTouchpadDebugView.updateHardwareState(touchpadHardwareState,
+                                deviceId));
+            }
+        });
     }
 
     /**
      * Notify the TouchpadDebugView of a new touchpad gesture.
      */
+    @AnyThread
     public void updateTouchpadGestureInfo(int gestureType, int deviceId) {
-        if (mTouchpadDebugView != null) {
-            mTouchpadDebugView.updateGestureInfo(gestureType, deviceId);
-        }
+        mHandler.post(() -> {
+            if (mTouchpadDebugView != null) {
+                mTouchpadDebugView.post(
+                        () -> mTouchpadDebugView.updateGestureInfo(gestureType, deviceId));
+            }
+        });
     }
 }
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 60056eb..6f50295 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -912,12 +912,13 @@
      * available ShareTarget definitions in this package.
      */
     public List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
-            @NonNull final IntentFilter filter) {
-        return getMatchingShareTargets(filter, null);
+            @NonNull final IntentFilter filter, final int callingUserId) {
+        return getMatchingShareTargets(filter, null, callingUserId);
     }
 
     List<ShortcutManager.ShareShortcutInfo> getMatchingShareTargets(
-            @NonNull final IntentFilter filter, @Nullable final String pkgName) {
+            @NonNull final IntentFilter filter, @Nullable final String pkgName,
+            final int callingUserId) {
         synchronized (mPackageItemLock) {
             final List<ShareTargetInfo> matchedTargets = new ArrayList<>();
             for (int i = 0; i < mShareTargets.size(); i++) {
@@ -941,7 +942,7 @@
             // included in the result
             findAll(shortcuts, ShortcutInfo::isNonManifestVisible,
                     ShortcutInfo.CLONE_REMOVE_FOR_APP_PREDICTION,
-                    pkgName, 0, /*getPinnedByAnyLauncher=*/ false);
+                    pkgName, callingUserId, /*getPinnedByAnyLauncher=*/ false);
 
             final List<ShortcutManager.ShareShortcutInfo> result = new ArrayList<>();
             for (int i = 0; i < shortcuts.size(); i++) {
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index a3ff195..5518bfa 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -2591,7 +2591,8 @@
             final List<ShortcutManager.ShareShortcutInfo> shortcutInfoList = new ArrayList<>();
             final ShortcutUser user = getUserShortcutsLocked(userId);
             user.forAllPackages(p -> shortcutInfoList.addAll(
-                    p.getMatchingShareTargets(filter, pkg)));
+                    p.getMatchingShareTargets(filter, pkg,
+                            mUserManagerInternal.getProfileParentId(userId))));
             return new ParceledListSlice<>(shortcutInfoList);
         }
     }
@@ -2623,7 +2624,8 @@
 
             final List<ShortcutManager.ShareShortcutInfo> matchedTargets =
                     getPackageShortcutsLocked(packageName, userId)
-                            .getMatchingShareTargets(filter);
+                            .getMatchingShareTargets(filter,
+                                    mUserManagerInternal.getProfileParentId(callingUserId));
             final int matchedSize = matchedTargets.size();
             for (int i = 0; i < matchedSize; i++) {
                 if (matchedTargets.get(i).getShortcutInfo().getId().equals(shortcutId)) {
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
index 153bb91..1ba2487 100644
--- a/services/core/java/com/android/server/utils/AnrTimer.java
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -130,22 +130,35 @@
     }
 
     /**
-     * Return true if freezing is enabled.  This has no effect if the service is not enabled.
+     * Return true if freezing is feature-enabled.  Freezing must still be enabled on a
+     * per-service basis.
      */
-    private static boolean anrTimerFreezerEnabled() {
+    private static boolean freezerFeatureEnabled() {
         return Flags.anrTimerFreezer();
     }
 
     /**
+     * Return true if tracing is feature-enabled.  This has no effect unless tracing is configured.
+     * Note that this does not represent any per-process overrides via an Injector.
+     */
+    public static boolean traceFeatureEnabled() {
+        return anrTimerServiceEnabled() && Flags.anrTimerTrace();
+    }
+
+    /**
      * This class allows test code to provide instance-specific overrides.
      */
     static class Injector {
-        boolean anrTimerServiceEnabled() {
+        boolean serviceEnabled() {
             return AnrTimer.anrTimerServiceEnabled();
         }
 
-        boolean anrTimerFreezerEnabled() {
-            return AnrTimer.anrTimerFreezerEnabled();
+        boolean freezerEnabled() {
+            return AnrTimer.freezerFeatureEnabled();
+        }
+
+        boolean traceEnabled() {
+            return AnrTimer.traceFeatureEnabled();
         }
     }
 
@@ -349,7 +362,7 @@
         mWhat = what;
         mLabel = label;
         mArgs = args;
-        boolean enabled = args.mInjector.anrTimerServiceEnabled() && nativeTimersSupported();
+        boolean enabled = args.mInjector.serviceEnabled() && nativeTimersSupported();
         mFeature = createFeatureSwitch(enabled);
     }
 
@@ -448,7 +461,7 @@
 
     /**
      * The FeatureDisabled class bypasses almost all AnrTimer logic.  It is used when the AnrTimer
-     * service is disabled via Flags.anrTimerServiceEnabled.
+     * service is disabled via Flags.anrTimerService().
      */
     private class FeatureDisabled extends FeatureSwitch {
         /** Start a timer by sending a message to the client's handler. */
@@ -515,7 +528,7 @@
 
     /**
      * The FeatureEnabled class enables the AnrTimer logic.  It is used when the AnrTimer service
-     * is enabled via Flags.anrTimerServiceEnabled.
+     * is enabled via Flags.anrTimerService().
      */
     private class FeatureEnabled extends FeatureSwitch {
 
@@ -533,7 +546,7 @@
         FeatureEnabled() {
             mNative = nativeAnrTimerCreate(mLabel,
                     mArgs.mExtend,
-                    mArgs.mFreeze && mArgs.mInjector.anrTimerFreezerEnabled());
+                    mArgs.mFreeze && mArgs.mInjector.freezerEnabled());
             if (mNative == 0) throw new IllegalArgumentException("unable to create native timer");
             synchronized (sAnrTimerList) {
                 sAnrTimerList.put(mNative, new WeakReference(AnrTimer.this));
@@ -550,7 +563,7 @@
                 // exist.
                 if (cancel(arg)) mTotalRestarted++;
 
-                int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs);
+                final int timerId = nativeAnrTimerStart(mNative, pid, uid, timeoutMs);
                 if (timerId > 0) {
                     mTimerIdMap.put(arg, timerId);
                     mTimerArgMap.put(timerId, arg);
@@ -895,7 +908,7 @@
     /** Dumpsys output, allowing for overrides. */
     @VisibleForTesting
     static void dump(@NonNull PrintWriter pw, boolean verbose, @NonNull Injector injector) {
-        if (!injector.anrTimerServiceEnabled()) return;
+        if (!injector.serviceEnabled()) return;
 
         final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
         ipw.println("AnrTimer statistics");
@@ -926,6 +939,18 @@
     }
 
     /**
+     * Set a trace specification.  The input is a set of strings.  On success, the function pushes
+     * the trace specification to all timers, and then returns a response message.  On failure,
+     * the function throws IllegalArgumentException and tracing is disabled.
+     *
+     * An empty specification has no effect other than returning the current trace specification.
+     */
+    @Nullable
+    public static String traceTimers(@Nullable String[] spec) {
+        return nativeAnrTimerTrace(spec);
+    }
+
+    /**
      * Return true if the native timers are supported.  Native timers are supported if the method
      * nativeAnrTimerSupported() can be executed and it returns true.
      */
@@ -981,6 +1006,15 @@
      */
     private static native boolean nativeAnrTimerRelease(long service, int timerId);
 
+    /**
+     * Configure tracing.  The input array is a set of words pulled from the command line.  All
+     * parsing happens inside the native layer.  The function returns a string which is either an
+     * error message (so nothing happened) or the current configuration after applying the config.
+     * Passing an null array or an empty array simply returns the current configuration.
+     * The function returns null if the native layer is not implemented.
+     */
+    private static native @Nullable String nativeAnrTimerTrace(@Nullable String[] config);
+
     /** Retrieve runtime dump information from the native layer. */
     private static native String[] nativeAnrTimerDump(long service);
 }
diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig
index 00ebb66..333287f 100644
--- a/services/core/java/com/android/server/utils/flags.aconfig
+++ b/services/core/java/com/android/server/utils/flags.aconfig
@@ -17,3 +17,10 @@
      bug: "325594551"
 }
 
+flag {
+     name: "anr_timer_trace"
+     namespace: "system_performance"
+     is_fixed_read_only: true
+     description: "When true, start a trace if an ANR timer reaches 50%"
+     bug: "352085328"
+}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 7aede8b..9da848a 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -38,6 +38,7 @@
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS;
+import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS_TRIM_TASKS;
@@ -1493,12 +1494,20 @@
             if (isExcludeFromRecents) {
                 if (DEBUG_RECENTS_TRIM_TASKS) {
                     Slog.d(TAG,
-                            "\texcludeFromRecents=true, taskIndex = " + taskIndex
-                                    + ", isOnHomeDisplay: " + task.isOnHomeDisplay());
+                            "\texcludeFromRecents=true,"
+                                + " taskIndex: " + taskIndex
+                                + " getTopVisibleActivity: " + task.getTopVisibleActivity()
+                                + " isOnHomeDisplay: " + task.isOnHomeDisplay());
                 }
                 // The Recents is only supported on default display now, we should only keep the
                 // most recent task of home display.
-                return (task.isOnHomeDisplay() && taskIndex == 0);
+                boolean isMostRecentTask;
+                if (enableRefactorTaskThumbnail()) {
+                    isMostRecentTask = task.getTopVisibleActivity() != null;
+                } else {
+                    isMostRecentTask = taskIndex == 0;
+                }
+                return (task.isOnHomeDisplay() && isMostRecentTask);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 1640ad3..4c4b4f6 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3856,16 +3856,8 @@
         }
         fillInsetsState(mLastReportedInsetsState, false /* copySources */);
         fillInsetsSourceControls(mLastReportedActiveControls, false /* copyControls */);
-        if (Flags.insetsControlChangedItem()) {
-            getProcess().scheduleClientTransactionItem(new WindowStateInsetsControlChangeItem(
-                    mClient, mLastReportedInsetsState, mLastReportedActiveControls));
-        } else {
-            try {
-                mClient.insetsControlChanged(mLastReportedInsetsState, mLastReportedActiveControls);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to deliver inset control state change to w=" + this, e);
-            }
-        }
+        getProcess().scheduleClientTransactionItem(new WindowStateInsetsControlChangeItem(
+                mClient, mLastReportedInsetsState, mLastReportedActiveControls));
     }
 
     @Override
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index cf96114..2836d46 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -19,6 +19,8 @@
 #include <sys/timerfd.h>
 #include <inttypes.h>
 #include <sys/stat.h>
+#include <unistd.h>
+#include <regex.h>
 
 #include <algorithm>
 #include <list>
@@ -26,6 +28,7 @@
 #include <set>
 #include <string>
 #include <vector>
+#include <map>
 
 #define LOG_TAG "AnrTimerService"
 #define ATRACE_TAG ATRACE_TAG_ACTIVITY_MANAGER
@@ -33,8 +36,8 @@
 
 #include <jni.h>
 #include <nativehelper/JNIHelp.h>
-#include "android_runtime/AndroidRuntime.h"
-#include "core_jni_helpers.h"
+#include <android_runtime/AndroidRuntime.h>
+#include <core_jni_helpers.h>
 
 #include <processgroup/processgroup.h>
 #include <utils/Log.h>
@@ -109,32 +112,336 @@
 // Return the name of the process whose pid is the input.  If the process does not exist, the
 // name will "notfound".
 std::string getProcessName(pid_t pid) {
-    char buffer[PATH_MAX];
-    snprintf(buffer, sizeof(buffer), "/proc/%d/cmdline", pid);
-    int fd = ::open(buffer, O_RDONLY);
-    if (fd >= 0) {
-        size_t pos = 0;
-        ssize_t result;
-        while (pos < sizeof(buffer)-1) {
-            result = ::read(fd, buffer + pos, (sizeof(buffer) - pos) - 1);
-            if (result <= 0) {
-                break;
-            }
-        }
-        ::close(fd);
-
-        if (result >= 0) {
-            buffer[pos] = 0;
+    char path[PATH_MAX];
+    snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
+    FILE* cmdline = fopen(path, "r");
+    if (cmdline != nullptr) {
+        char name[PATH_MAX];
+        char const *retval = fgets(name, sizeof(name), cmdline);
+        fclose(cmdline);
+        if (retval == nullptr) {
+            return std::string("unknown");
         } else {
-            snprintf(buffer, sizeof(buffer), "err: %s", strerror(errno));
+            return std::string(name);
         }
     } else {
-        snprintf(buffer, sizeof(buffer), "notfound");
+        return std::string("notfound");
     }
-    return std::string(buffer);
 }
 
 /**
+ * Three wrappers of the trace utilities, which hard-code the timer track.
+ */
+void traceBegin(const char* msg, int cookie) {
+    ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, msg, cookie);
+}
+
+void traceEnd(int cookie) {
+    ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+}
+
+void traceEvent(const char* msg) {
+    ATRACE_INSTANT_FOR_TRACK(ANR_TIMER_TRACK, msg);
+}
+
+/**
+ * This class captures tracing information for processes tracked by an AnrTimer.  A user can
+ * configure tracing to have the AnrTimerService emit extra information for watched processes.
+ * singleton.
+ *
+ * The tracing configuration has two components: process selection and an optional early action.
+ *
+ *   Processes are selected in one of three ways:
+ *    1. A list of numeric linux process IDs.
+ *    2. A regular expression, matched against process names.
+ *    3. The keyword "all", to trace every process that uses an AnrTimer.
+ *   Perfetto trace events are always emitted for every operation on a traced process.
+ *
+ *   An early action occurs before the scheduled timeout.  The early timeout is specified as a
+ *   percentage (integer value in the range 0:100) of the programmed timeout.  The AnrTimer will
+ *   execute the early action at the early timeout.  The early action may terminate the timer.
+ *
+ *   There is one early action:
+ *    1. Expire - consider the AnrTimer expired and report it to the upper layers.
+ */
+class AnrTimerTracer {
+  public:
+    // Actions that can be taken when an early  timer expires.
+    enum EarlyAction {
+        // Take no action.  This is the value used when tracing is disabled.
+        None,
+        // Trace the timer but take no other action.
+        Trace,
+        // Report timer expiration to the upper layers.  This is terminal, in that
+        Expire,
+    };
+
+    // The trace information for a single timer.
+    struct TraceConfig {
+        bool enabled = false;
+        EarlyAction action = None;
+        int earlyTimeout = 0;
+    };
+
+    AnrTimerTracer() {
+        AutoMutex _l(lock_);
+        resetLocked();
+    }
+
+    // Return the TraceConfig for a process.
+    TraceConfig getConfig(int pid) {
+        AutoMutex _l(lock_);
+        // The most likely situation: no tracing is configured.
+        if (!config_.enabled) return {};
+        if (matchAllPids_) return config_;
+        if (watched_.contains(pid)) return config_;
+        if (!matchNames_) return {};
+        if (matchedPids_.contains(pid)) return config_;
+        if (unmatchedPids_.contains(pid)) return {};
+        std::string proc_name = getProcessName(pid);
+        bool matched = regexec(&regex_, proc_name.c_str(), 0, 0, 0) == 0;
+        if (matched) {
+            matchedPids_.insert(pid);
+            return config_;
+        } else {
+            unmatchedPids_.insert(pid);
+            return {};
+        }
+    }
+
+    // Set the trace configuration.  The input is a string that contains key/value pairs of the
+    // form "key=value".  Pairs are separated by spaces.  The function returns a string status.
+    // On success, the normalized config is returned.  On failure, the configuration reset the
+    // result contains an error message.  As a special case, an empty set of configs, or a
+    // config that contains only the keyword "show", will do nothing except return the current
+    // configuration.  On any error, all tracing is disabled.
+    std::pair<bool, std::string> setConfig(const std::vector<std::string>& config) {
+        AutoMutex _l(lock_);
+        if (config.size() == 0) {
+            // Implicit "show"
+            return { true, currentConfigLocked() };
+        } else if (config.size() == 1) {
+            // Process the one-word commands
+            const char* s = config[0].c_str();
+            if (strcmp(s, "show") == 0) {
+                return { true, currentConfigLocked() };
+            } else if (strcmp(s, "off") == 0) {
+                resetLocked();
+                return { true, currentConfigLocked() };
+            } else if (strcmp(s, "help") == 0) {
+                return { true, help() };
+            }
+        } else if (config.size() > 2) {
+            return { false, "unexpected values in config" };
+        }
+
+        // Barring an error in the remaining specification list, tracing will be enabled.
+        resetLocked();
+        // Fetch the process specification.  This must be the first configuration entry.
+        {
+            auto result = setTracedProcess(config[0]);
+            if (!result.first) return result;
+        }
+
+        // Process optional actions.
+        if (config.size() > 1) {
+            auto result = setTracedAction(config[1]);
+            if (!result.first) return result;
+        }
+
+        // Accept the result.
+        config_.enabled = true;
+        return { true, currentConfigLocked() };
+    }
+
+  private:
+    // Identify the processes to be traced.
+    std::pair<bool, std::string> setTracedProcess(std::string config) {
+        const char* s = config.c_str();
+        const char* word = nullptr;
+
+        if (strcmp(s, "pid=all") == 0) {
+            matchAllPids_ = true;
+        } else if ((word = startsWith(s, "pid=")) != nullptr) {
+            int p;
+            int n;
+            while (sscanf(word, "%d%n", &p, &n) == 1) {
+                watched_.insert(p);
+                word += n;
+                if (*word == ',') word++;
+            }
+            if (*word != 0) {
+                return { false, "invalid pid list" };
+            }
+            config_.action = Trace;
+        } else if ((word = startsWith(s, "name=")) != nullptr) {
+            if (matchNames_) {
+                regfree(&regex_);
+                matchNames_ = false;
+            }
+            if (regcomp(&regex_, word, REG_EXTENDED) != 0) {
+                return { false, "invalid regex" };
+            }
+            matchNames_ = true;
+            namePattern_ = word;
+            config_.action = Trace;
+        } else {
+            return { false, "no process specified" };
+        }
+        return { true, "" };
+    }
+
+    // Set the action to be taken on a traced process.  The incoming default action is Trace;
+    // this method may overwrite that action.
+    std::pair<bool, std::string> setTracedAction(std::string config) {
+        const char* s = config.c_str();
+        const char* word = nullptr;
+        if (sscanf(s, "expire=%d", &config_.earlyTimeout) == 1) {
+            if (config_.earlyTimeout < 0) {
+                return { false, "invalid expire timeout" };
+            }
+            config_.action = Expire;
+        } else {
+            return { false, std::string("cannot parse action ") + s };
+        }
+        return { true, "" };
+    }
+
+    // Return the string value of an action.
+    static const char* toString(EarlyAction action) {
+        switch (action) {
+            case None: return "none";
+            case Trace: return "trace";
+            case Expire: return "expire";
+        }
+        return "unknown";
+    }
+
+    // Return the action represented by the string.
+    static EarlyAction fromString(const char* action) {
+        if (strcmp(action, "expire") == 0) return Expire;
+        return None;
+    }
+
+    // Return the help message.  This has everything except the invocation command.
+    static std::string help() {
+        static const char* msg =
+                "help     show this message\n"
+                "show     report the current configuration\n"
+                "off      clear the current configuration, turning off all tracing\n"
+                "spec...  configure tracing according to the specification list\n"
+                "  action=<action>     what to do when a split timer expires\n"
+                "    expire            expire the timer to the upper levels\n"
+                "    event             generate extra trace events\n"
+                "  pid=<pid>[,<pid>]   watch the processes in the pid list\n"
+                "  pid=all             watch every process in the system\n"
+                "  name=<regex>        watch the processes whose name matches the regex\n";
+        return msg;
+    }
+
+    // A small convenience function for parsing.  If the haystack starts with the needle and the
+    // haystack has at least one more character following, return a pointer to the following
+    // character.  Otherwise return null.
+    static const char* startsWith(const char* haystack, const char* needle) {
+        if (strncmp(haystack, needle, strlen(needle)) == 0 && strlen(haystack) + strlen(needle)) {
+            return haystack + strlen(needle);
+        }
+        return nullptr;
+    }
+
+    // Return the currently watched pids.  The lock must be held.
+    std::string watchedPidsLocked() const {
+        if (watched_.size() == 0) return "none";
+        bool first = true;
+        std::string result = "";
+        for (auto i = watched_.cbegin(); i != watched_.cend(); i++) {
+            if (first) {
+                result += StringPrintf("%d", *i);
+            } else {
+                result += StringPrintf(",%d", *i);
+            }
+        }
+        return result;
+    }
+
+    // Return the current configuration, in a form that can be consumed by setConfig().
+    std::string currentConfigLocked() const {
+        if (!config_.enabled) return "off";
+        std::string result;
+        if (matchAllPids_) {
+            result = "pid=all";
+        } else if (matchNames_) {
+            result = StringPrintf("name=\"%s\"", namePattern_.c_str());
+        } else {
+            result = std::string("pid=") + watchedPidsLocked();
+        }
+        switch (config_.action) {
+            case None:
+                break;
+            case Trace:
+                // The default action is Trace
+                break;
+            case Expire:
+                result += StringPrintf(" %s=%d", toString(config_.action), config_.earlyTimeout);
+                break;
+        }
+        return result;
+    }
+
+    // Reset the current configuration.
+    void resetLocked() {
+        if (!config_.enabled) return;
+
+        config_.enabled = false;
+        config_.earlyTimeout = 0;
+        config_.action = {};
+        matchAllPids_ = false;
+        watched_.clear();
+        if (matchNames_) regfree(&regex_);
+        matchNames_ = false;
+        namePattern_ = "";
+        matchedPids_.clear();
+        unmatchedPids_.clear();
+    }
+
+    // The lock for all operations
+    mutable Mutex lock_;
+
+    // The current tracing information, when a process matches.
+    TraceConfig config_;
+
+    // A short-hand flag that causes all processes to be tracing without the overhead of
+    // searching any of the maps.
+    bool matchAllPids_;
+
+    // A set of process IDs that should be traced.  This is updated directly in setConfig()
+    // and only includes pids that were explicitly called out in the configuration.
+    std::set<pid_t> watched_;
+
+    // Name mapping is a relatively expensive operation, since the process name must be fetched
+    // from the /proc file system and then a regex must be evaluated.  However, name mapping is
+    // useful to ensure processes are traced at the moment they start.  To make this faster, a
+    // process's name is matched only once, and the result is stored in the matchedPids_ or
+    // unmatchedPids_ set, as appropriate.  This can lead to confusion if a process changes its
+    // name after it starts.
+
+    // The global flag that enables name matching.  If this is disabled then all name matching
+    // is disabled.
+    bool matchNames_;
+
+    // The regular expression that matches processes to be traced.  This is saved for logging.
+    std::string namePattern_;
+
+    // The compiled regular expression.
+    regex_t regex_;
+
+    // The set of all pids that whose process names match (or do not match) the name regex.
+    // There is one set for pids that match and one set for pids that do not match.
+    std::set<pid_t> matchedPids_;
+    std::set<pid_t> unmatchedPids_;
+};
+
+/**
  * This class encapsulates the anr timer service.  The service manages a list of individual
  * timers.  A timer is either Running or Expired.  Once started, a timer may be canceled or
  * accepted.  Both actions collect statistics about the timer and then delete it.  An expired
@@ -177,7 +484,7 @@
      * traditional void* and Java object pointer.  The remaining parameters are
      * configuration options.
      */
-    AnrTimerService(char const* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*,
+    AnrTimerService(const char* label, notifier_t notifier, void* cookie, jweak jtimer, Ticker*,
                     bool extend, bool freeze);
 
     // Delete the service and clean up memory.
@@ -211,6 +518,11 @@
     // Release a timer.  The timer must be in the expired list.
     bool release(timer_id_t);
 
+    // Configure a trace specification to trace selected timers.  See AnrTimerTracer for details.
+    static std::pair<bool, std::string> trace(const std::vector<std::string>& spec) {
+        return tracer_.setConfig(spec);
+    }
+
     // Return the Java object associated with this instance.
     jweak jtimer() const {
         return notifierObject_;
@@ -221,7 +533,7 @@
 
   private:
     // The service cannot be copied.
-    AnrTimerService(AnrTimerService const&) = delete;
+    AnrTimerService(const AnrTimerService&) = delete;
 
     // Insert a timer into the running list.  The lock must be held by the caller.
     void insertLocked(const Timer&);
@@ -230,7 +542,7 @@
     Timer removeLocked(timer_id_t timerId);
 
     // Add a timer to the expired list.
-    void addExpiredLocked(Timer const&);
+    void addExpiredLocked(const Timer&);
 
     // Scrub the expired list by removing all entries for non-existent processes.  The expired
     // lock must be held by the caller.
@@ -240,10 +552,10 @@
     static const char* statusString(Status);
 
     // The name of this service, for logging.
-    std::string const label_;
+    const std::string label_;
 
     // The callback that is invoked when a timer expires.
-    notifier_t const notifier_;
+    const notifier_t notifier_;
 
     // The two cookies passed to the notifier.
     void* notifierCookie_;
@@ -289,8 +601,13 @@
 
     // The clock used by this AnrTimerService.
     Ticker *ticker_;
+
+    // The global tracing specification.
+    static AnrTimerTracer tracer_;
 };
 
+AnrTimerTracer AnrTimerService::tracer_;
+
 class AnrTimerService::ProcessStats {
   public:
     nsecs_t cpu_time;
@@ -337,14 +654,23 @@
 class AnrTimerService::Timer {
   public:
     // A unique ID assigned when the Timer is created.
-    timer_id_t const id;
+    const timer_id_t id;
 
     // The creation parameters.  The timeout is the original, relative timeout.
-    int const pid;
-    int const uid;
-    nsecs_t const timeout;
-    bool const extend;
-    bool const freeze;
+    const int pid;
+    const int uid;
+    const nsecs_t timeout;
+    // True if the timer may be extended.
+    const bool extend;
+    // True if process should be frozen when its timer expires.
+    const bool freeze;
+    // This is a percentage between 0 and 100.  If it is non-zero then timer will fire at
+    // timeout*split/100, and the EarlyAction will be invoked.  The timer may continue running
+    // or may expire, depending on the action.  Thus, this value "splits" the timeout into two
+    // pieces.
+    const int split;
+    // The action to take if split (above) is non-zero, when the timer reaches the split point.
+    const AnrTimerTracer::EarlyAction action;
 
     // The state of this timer.
     Status status;
@@ -355,6 +681,9 @@
     // The scheduled timeout.  This is an absolute time.  It may be extended.
     nsecs_t scheduled;
 
+    // True if this timer is split and in its second half
+    bool splitting;
+
     // True if this timer has been extended.
     bool extended;
 
@@ -367,22 +696,10 @@
 
     // The default constructor is used to create timers that are Invalid, representing the "not
     // found" condition when a collection is searched.
-    Timer() :
-            id(NOTIMER),
-            pid(0),
-            uid(0),
-            timeout(0),
-            extend(false),
-            freeze(false),
-            status(Invalid),
-            started(0),
-            scheduled(0),
-            extended(false),
-            frozen(false) {
-    }
+    Timer() : Timer(NOTIMER) { }
 
-    // This constructor creates a timer with the specified id.  This can be used as the argument
-    // to find().
+    // This constructor creates a timer with the specified id and everything else set to
+    // "empty".  This can be used as the argument to find().
     Timer(timer_id_t id) :
             id(id),
             pid(0),
@@ -390,29 +707,37 @@
             timeout(0),
             extend(false),
             freeze(false),
+            split(0),
+            action(AnrTimerTracer::None),
             status(Invalid),
             started(0),
             scheduled(0),
+            splitting(false),
             extended(false),
             frozen(false) {
     }
 
     // Create a new timer.  This starts the timer.
-    Timer(int pid, int uid, nsecs_t timeout, bool extend, bool freeze) :
+    Timer(int pid, int uid, nsecs_t timeout, bool extend, bool freeze,
+          AnrTimerTracer::TraceConfig trace) :
             id(nextId()),
             pid(pid),
             uid(uid),
             timeout(timeout),
             extend(extend),
             freeze(pid != 0 && freeze),
+            split(trace.earlyTimeout),
+            action(trace.action),
             status(Running),
             started(now()),
-            scheduled(started + timeout),
+            scheduled(started + (split > 0 ? (timeout*split)/100 : timeout)),
+            splitting(false),
             extended(false),
             frozen(false) {
         if (extend && pid != 0) {
             initial.fill(pid);
         }
+
         // A zero-pid is odd but it means the upper layers will never ANR the process.  Freezing
         // is always disabled.  (It won't work anyway, but disabling it avoids error messages.)
         ALOGI_IF(DEBUG_ERROR && pid == 0, "error: zero-pid %s", toString().c_str());
@@ -434,6 +759,23 @@
     // returns false if the timer is eligible for extension.  If the function returns false, the
     // scheduled time is updated.
     bool expire() {
+        if (split > 0 && !splitting) {
+            scheduled = started + timeout;
+            splitting = true;
+            event("split");
+            switch (action) {
+                case AnrTimerTracer::None:
+                case AnrTimerTracer::Trace:
+                    break;
+                case AnrTimerTracer::Expire:
+                    status = Expired;
+                    maybeFreezeProcess();
+                    event("expire");
+                    break;
+            }
+            return status == Expired;
+        }
+
         nsecs_t extension = 0;
         if (extend && !extended) {
             // Only one extension is permitted.
@@ -525,15 +867,15 @@
 
         char tag[PATH_MAX];
         snprintf(tag, sizeof(tag), "freeze(pid=%d,uid=%d)", pid, uid);
-        ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, tag, cookie);
+        traceBegin(tag, cookie);
         if (SetProcessProfiles(uid, pid, {"Frozen"})) {
             ALOGI("freeze %s name=%s", toString().c_str(), getName().c_str());
             frozen = true;
-            ATRACE_ASYNC_FOR_TRACK_BEGIN(ANR_TIMER_TRACK, "frozen", cookie+1);
+            traceBegin("frozen", cookie+1);
         } else {
             ALOGE("error: freezing %s name=%s error=%s",
                   toString().c_str(), getName().c_str(), strerror(errno));
-            ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+            traceEnd(cookie);
         }
     }
 
@@ -543,7 +885,7 @@
         // See maybeFreezeProcess for an explanation of the cookie.
         const uint32_t cookie = id << 1;
 
-        ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie+1);
+        traceEnd(cookie+1);
         if (SetProcessProfiles(uid, pid, {"Unfrozen"})) {
             ALOGI("unfreeze %s name=%s", toString().c_str(), getName().c_str());
             frozen = false;
@@ -551,7 +893,7 @@
             ALOGE("error: unfreezing %s name=%s error=%s",
                   toString().c_str(), getName().c_str(), strerror(errno));
         }
-        ATRACE_ASYNC_FOR_TRACK_END(ANR_TIMER_TRACK, cookie);
+        traceEnd(cookie);
     }
 
     // Get the next free ID.  NOTIMER is never returned.
@@ -564,12 +906,17 @@
     }
 
     // Log an event, non-verbose.
-    void event(char const* tag) {
+    void event(const char* tag) {
         event(tag, false);
     }
 
     // Log an event, guarded by the debug flag.
-    void event(char const* tag, bool verbose) {
+    void event(const char* tag, bool verbose) {
+        if (action != AnrTimerTracer::None) {
+            char msg[PATH_MAX];
+            snprintf(msg, sizeof(msg), "%s(pid=%d)", tag, pid);
+            traceEvent(msg);
+        }
         if (verbose) {
             char name[PATH_MAX];
             ALOGI_IF(DEBUG_TIMER, "event %s %s name=%s",
@@ -594,12 +941,12 @@
     struct Entry {
         const nsecs_t scheduled;
         const timer_id_t id;
-        AnrTimerService* const service;
+        AnrTimerService* service;
 
         Entry(nsecs_t scheduled, timer_id_t id, AnrTimerService* service) :
                 scheduled(scheduled), id(id), service(service) {};
 
-        bool operator<(const Entry &r) const {
+        bool operator<(const Entry& r) const {
             return scheduled == r.scheduled ? id < r.id : scheduled < r.scheduled;
         }
     };
@@ -664,7 +1011,7 @@
     }
 
     // Remove every timer associated with the service.
-    void remove(AnrTimerService const* service) {
+    void remove(const AnrTimerService* service) {
         AutoMutex _l(lock_);
         timer_id_t front = headTimerId();
         for (auto i = running_.begin(); i != running_.end(); ) {
@@ -746,7 +1093,7 @@
     // scheduled expiration time of the first entry.
     void restartLocked() {
         if (!running_.empty()) {
-            Entry const x = *(running_.cbegin());
+            const Entry x = *(running_.cbegin());
             nsecs_t delay = x.scheduled - now();
             // Force a minimum timeout of 10ns.
             if (delay < 10) delay = 10;
@@ -807,7 +1154,7 @@
 std::atomic<size_t> AnrTimerService::Ticker::idGen_;
 
 
-AnrTimerService::AnrTimerService(char const* label, notifier_t notifier, void* cookie,
+AnrTimerService::AnrTimerService(const char* label, notifier_t notifier, void* cookie,
             jweak jtimer, Ticker* ticker, bool extend, bool freeze) :
         label_(label),
         notifier_(notifier),
@@ -841,7 +1188,7 @@
 
 AnrTimerService::timer_id_t AnrTimerService::start(int pid, int uid, nsecs_t timeout) {
     AutoMutex _l(lock_);
-    Timer t(pid, uid, timeout, extend_, freeze_);
+    Timer t(pid, uid, timeout, extend_, freeze_, tracer_.getConfig(pid));
     insertLocked(t);
     t.start();
     counters_.started++;
@@ -918,7 +1265,7 @@
     return okay;
 }
 
-void AnrTimerService::addExpiredLocked(Timer const& timer) {
+void AnrTimerService::addExpiredLocked(const Timer& timer) {
     scrubExpiredLocked();
     expired_.insert(timer);
 }
@@ -1077,7 +1424,7 @@
     ScopedUtfChars name(env, jname);
     jobject timer = env->NewWeakGlobalRef(jtimer);
     AnrTimerService* service = new AnrTimerService(name.c_str(),
-            anrNotify, &gAnrArgs, timer, gAnrArgs.ticker, extend, freeze);
+        anrNotify, &gAnrArgs, timer, gAnrArgs.ticker, extend, freeze);
     return reinterpret_cast<jlong>(service);
 }
 
@@ -1122,6 +1469,19 @@
     return toService(ptr)->release(timerId);
 }
 
+jstring anrTimerTrace(JNIEnv* env, jclass, jobjectArray jconfig) {
+    if (!nativeSupportEnabled) return nullptr;
+    std::vector<std::string> config;
+    const jsize jlen = jconfig == nullptr ? 0 : env->GetArrayLength(jconfig);
+    for (size_t i = 0; i < jlen; i++) {
+        jstring je = static_cast<jstring>(env->GetObjectArrayElement(jconfig, i));
+        ScopedUtfChars e(env, je);
+        config.push_back(e.c_str());
+    }
+    auto r = AnrTimerService::trace(config);
+    return env->NewStringUTF(r.second.c_str());
+}
+
 jobjectArray anrTimerDump(JNIEnv *env, jclass, jlong ptr) {
     if (!nativeSupportEnabled) return nullptr;
     std::vector<std::string> stats = toService(ptr)->getDump();
@@ -1134,22 +1494,23 @@
 }
 
 static const JNINativeMethod methods[] = {
-    {"nativeAnrTimerSupported", "()Z",  (void*) anrTimerSupported},
-    {"nativeAnrTimerCreate",   "(Ljava/lang/String;ZZ)J", (void*) anrTimerCreate},
-    {"nativeAnrTimerClose",    "(J)I",     (void*) anrTimerClose},
-    {"nativeAnrTimerStart",    "(JIIJ)I",  (void*) anrTimerStart},
-    {"nativeAnrTimerCancel",   "(JI)Z",    (void*) anrTimerCancel},
-    {"nativeAnrTimerAccept",   "(JI)Z",    (void*) anrTimerAccept},
-    {"nativeAnrTimerDiscard",  "(JI)Z",    (void*) anrTimerDiscard},
-    {"nativeAnrTimerRelease",  "(JI)Z",    (void*) anrTimerRelease},
-    {"nativeAnrTimerDump",     "(J)[Ljava/lang/String;", (void*) anrTimerDump},
+    {"nativeAnrTimerSupported",   "()Z",        (void*) anrTimerSupported},
+    {"nativeAnrTimerCreate",      "(Ljava/lang/String;ZZ)J", (void*) anrTimerCreate},
+    {"nativeAnrTimerClose",       "(J)I",       (void*) anrTimerClose},
+    {"nativeAnrTimerStart",       "(JIIJ)I",    (void*) anrTimerStart},
+    {"nativeAnrTimerCancel",      "(JI)Z",      (void*) anrTimerCancel},
+    {"nativeAnrTimerAccept",      "(JI)Z",      (void*) anrTimerAccept},
+    {"nativeAnrTimerDiscard",     "(JI)Z",      (void*) anrTimerDiscard},
+    {"nativeAnrTimerRelease",     "(JI)Z",      (void*) anrTimerRelease},
+    {"nativeAnrTimerTrace",       "([Ljava/lang/String;)Ljava/lang/String;", (void*) anrTimerTrace},
+    {"nativeAnrTimerDump",        "(J)[Ljava/lang/String;", (void*) anrTimerDump},
 };
 
 } // anonymous namespace
 
 int register_android_server_utils_AnrTimer(JNIEnv* env)
 {
-    static const char *className = "com/android/server/utils/AnrTimer";
+    static const char* className = "com/android/server/utils/AnrTimer";
     jniRegisterNativeMethods(env, className, methods, NELEM(methods));
 
     nativeSupportEnabled = NATIVE_SUPPORT;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
index 598d3a3..b745e6a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java
@@ -32,6 +32,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -64,6 +65,7 @@
 import android.graphics.Region;
 import android.os.Handler;
 import android.os.Message;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
@@ -105,6 +107,7 @@
 import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.function.IntConsumer;
 
@@ -700,6 +703,15 @@
     }
 
     @Test
+    public void testIntervalsOf_sendMotionEventInfo_returnMatchIntervals() {
+        FullScreenMagnificationGestureHandler.MotionEventInfo upEventQueue =
+                createEventQueue(ACTION_UP, 0, 100, 300);
+
+        List<Long> upIntervals = mMgh.mDetectingState.intervalsOf(upEventQueue, ACTION_UP);
+        assertEquals(Arrays.asList(100L, 200L), upIntervals);
+    }
+
+    @Test
     public void testMagnifierDeactivates_shortcutTriggeredState_returnToIdleState() {
         goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
 
@@ -2294,6 +2306,31 @@
         return event;
     }
 
+    private FullScreenMagnificationGestureHandler.MotionEventInfo createEventQueue(
+            int eventType, long... delays) {
+        FullScreenMagnificationGestureHandler.MotionEventInfo eventQueue = null;
+        long currentTime = SystemClock.uptimeMillis();
+
+        for (int i = 0; i < delays.length; i++) {
+            MotionEvent event = MotionEvent.obtain(currentTime + delays[i],
+                    currentTime + delays[i], eventType, 0, 0, 0);
+
+            FullScreenMagnificationGestureHandler.MotionEventInfo info =
+                    FullScreenMagnificationGestureHandler.MotionEventInfo
+                    .obtain(event, MotionEvent.obtain(event), 0);
+
+            if (eventQueue == null) {
+                eventQueue = info;
+            } else {
+                FullScreenMagnificationGestureHandler.MotionEventInfo tail = eventQueue;
+                while (tail.getNext() != null) {
+                    tail = tail.getNext();
+                }
+                tail.setNext(info);
+            }
+        }
+        return eventQueue;
+    }
 
     private String stateDump() {
         return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages();
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
index b09e9b1..54282ff 100644
--- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -119,7 +119,7 @@
      */
     private class TestInjector extends AnrTimer.Injector {
         @Override
-        boolean anrTimerServiceEnabled() {
+        boolean serviceEnabled() {
             return mEnabled;
         }
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index 8f3d3c5..e0344d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -70,7 +70,10 @@
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.SparseBooleanArray;
@@ -79,9 +82,12 @@
 
 import androidx.test.filters.MediumTest;
 
+import com.android.launcher3.Flags;
 import com.android.server.wm.RecentTasks.Callbacks;
 
 import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -122,6 +128,10 @@
 
     private CallbacksRecorder mCallbacksRecorder;
 
+    @Rule
+    public SetFlagsRule mSetFlagsRule =
+            new SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
     @Before
     public void setUp() throws Exception {
         mTaskPersister = new TestTaskPersister(mContext.getFilesDir());
@@ -697,14 +707,31 @@
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
     public void testVisibleTasks_excludedFromRecents() {
+        testVisibleTasks_excludedFromRecents_internal();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
+    public void testVisibleTasks_excludedFromRecents_withRefactorFlag() {
+        testVisibleTasks_excludedFromRecents_internal();
+    }
+
+    private void testVisibleTasks_excludedFromRecents_internal() {
         mRecentTasks.setParameters(-1 /* min */, 4 /* max */, -1 /* ms */);
 
-        Task excludedTask1 = createTaskBuilder(".ExcludedTask1")
+        Task invisibleExcludedTask = createTaskBuilder(".ExcludedTask1")
                 .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .setCreateActivity(true)
                 .build();
-        Task excludedTask2 = createTaskBuilder(".ExcludedTask2")
+        ActivityRecord activityRecord = invisibleExcludedTask.getTopMostActivity();
+        activityRecord.setVisibleRequested(false);
+        activityRecord.setVisible(false);
+
+        Task visibleExcludedTask = createTaskBuilder(".ExcludedTask2")
                 .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .setCreateActivity(true)
                 .build();
         Task detachedExcludedTask = createTaskBuilder(".DetachedExcludedTask")
                 .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
@@ -718,18 +745,79 @@
         assertFalse(detachedExcludedTask.isAttached());
 
         mRecentTasks.add(detachedExcludedTask);
-        mRecentTasks.add(excludedTask1);
+        mRecentTasks.add(invisibleExcludedTask);
         mRecentTasks.add(mTasks.get(0));
         mRecentTasks.add(mTasks.get(1));
         mRecentTasks.add(mTasks.get(2));
-        mRecentTasks.add(excludedTask2);
+        mRecentTasks.add(visibleExcludedTask);
 
-        // Except the first-most excluded task, other excluded tasks should be trimmed.
-        triggerTrimAndAssertTrimmed(excludedTask1, detachedExcludedTask);
+        // Excluded tasks should be trimmed, except those with a visible activity.
+        triggerTrimAndAssertTrimmed(invisibleExcludedTask, detachedExcludedTask);
     }
 
     @Test
+    @Ignore("b/342627272")
+    @DisableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
+    public void testVisibleTasks_excludedFromRecents_visibleTaskNotFirstTask() {
+        testVisibleTasks_excludedFromRecents_visibleTaskNotFirstTask_internal();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
+    public void testVisibleTasks_excludedFromRecents_visibleTaskNotFirstTask_withRefactorFlag() {
+        testVisibleTasks_excludedFromRecents_visibleTaskNotFirstTask_internal();
+    }
+
+    private void testVisibleTasks_excludedFromRecents_visibleTaskNotFirstTask_internal() {
+        mRecentTasks.setParameters(-1 /* min */, 4 /* max */, -1 /* ms */);
+
+        Task invisibleExcludedTask = createTaskBuilder(".ExcludedTask1")
+                .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .setCreateActivity(true)
+                .build();
+        ActivityRecord activityRecord = invisibleExcludedTask.getTopMostActivity();
+        activityRecord.setVisibleRequested(false);
+        activityRecord.setVisible(false);
+
+        Task visibleExcludedTask = createTaskBuilder(".ExcludedTask2")
+                .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .setCreateActivity(true)
+                .build();
+        Task detachedExcludedTask = createTaskBuilder(".DetachedExcludedTask")
+                .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .build();
+
+        // Move home to front so other task can satisfy the condition in RecentTasks#isTrimmable.
+        mRootWindowContainer.getDefaultTaskDisplayArea().getRootHomeTask().moveToFront("test");
+        // Avoid Task#autoRemoveFromRecents when removing from parent.
+        detachedExcludedTask.setHasBeenVisible(true);
+        detachedExcludedTask.removeImmediately();
+        assertFalse(detachedExcludedTask.isAttached());
+
+        mRecentTasks.add(detachedExcludedTask);
+        mRecentTasks.add(visibleExcludedTask);
+        mRecentTasks.add(mTasks.get(0));
+        mRecentTasks.add(mTasks.get(1));
+        mRecentTasks.add(mTasks.get(2));
+        mRecentTasks.add(invisibleExcludedTask);
+
+        // Excluded tasks should be trimmed, except those with a visible activity.
+        triggerTrimAndAssertTrimmed(invisibleExcludedTask, detachedExcludedTask);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
     public void testVisibleTasks_excludedFromRecents_firstTaskNotVisible() {
+        testVisibleTasks_excludedFromRecents_firstTaskNotVisible_internal();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_REFACTOR_TASK_THUMBNAIL)
+    public void testVisibleTasks_excludedFromRecents_firstTaskNotVisible_withRefactorFlag() {
+        testVisibleTasks_excludedFromRecents_firstTaskNotVisible_internal();
+    }
+
+    private void testVisibleTasks_excludedFromRecents_firstTaskNotVisible_internal() {
         // Create some set of tasks, some of which are visible and some are not
         Task homeTask = createTaskBuilder("com.android.pkg1", ".HomeTask")
                 .setParentTask(mTaskContainer.getRootHomeTask())
@@ -738,11 +826,12 @@
         mRecentTasks.add(homeTask);
         Task excludedTask1 = createTaskBuilder(".ExcludedTask1")
                 .setFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+                .setCreateActivity(true)
                 .build();
         excludedTask1.mUserSetupComplete = true;
         mRecentTasks.add(excludedTask1);
 
-        // Expect that the first visible excluded-from-recents task is visible
+        // Expect that the visible excluded-from-recents task is visible
         assertGetRecentTasksOrder(0 /* flags */, excludedTask1);
     }
 
@@ -1439,9 +1528,9 @@
      */
     private void assertGetRecentTasksOrder(int getRecentTaskFlags, Task... expectedTasks) {
         List<RecentTaskInfo> infos = getRecentTasks(getRecentTaskFlags);
-        assertTrue(expectedTasks.length == infos.size());
-        for (int i = 0; i < infos.size(); i++)  {
-            assertTrue(expectedTasks[i].mTaskId == infos.get(i).taskId);
+        assertEquals(expectedTasks.length, infos.size());
+        for (int i = 0; i < infos.size(); i++) {
+            assertEquals(expectedTasks[i].mTaskId, infos.get(i).taskId);
         }
     }
 
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 2ef0573..47f6764 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9763,7 +9763,7 @@
      * users to switch to using satellite emergency messaging.</li>
      * </ul>
      * <p>
-     * The default value is 300 seconds.
+     * The default value is 180 seconds.
      */
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT =
@@ -11257,7 +11257,7 @@
                 KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE,
                 PersistableBundle.EMPTY);
         sDefaults.putBoolean(KEY_SATELLITE_ATTACH_SUPPORTED_BOOL, false);
-        sDefaults.putInt(KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT, 300);
+        sDefaults.putInt(KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT, 180);
         sDefaults.putIntArray(KEY_NTN_LTE_RSRP_THRESHOLDS_INT_ARRAY,
                 // Boundaries: [-140 dBm, -44 dBm]
                 new int[]{
diff --git a/tests/broadcasts/OWNERS b/tests/broadcasts/OWNERS
new file mode 100644
index 0000000..d2e1f81
--- /dev/null
+++ b/tests/broadcasts/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316181
+include platform/frameworks/base:/BROADCASTS_OWNERS
diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp
new file mode 100644
index 0000000..47166a7
--- /dev/null
+++ b/tests/broadcasts/unit/Android.bp
@@ -0,0 +1,45 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_framework_backstage_power",
+}
+
+android_test {
+    name: "BroadcastUnitTests",
+    srcs: ["src/**/*.java"],
+    defaults: [
+        "modules-utils-extended-mockito-rule-defaults",
+    ],
+    static_libs: [
+        "androidx.test.runner",
+        "androidx.test.rules",
+        "androidx.test.ext.junit",
+        "mockito-target-extended-minus-junit4",
+        "truth",
+        "flag-junit",
+        "android.app.flags-aconfig-java",
+    ],
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+}
diff --git a/tests/broadcasts/unit/AndroidManifest.xml b/tests/broadcasts/unit/AndroidManifest.xml
new file mode 100644
index 0000000..e9c5248
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.broadcasts.unit" >
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.broadcasts.unit"
+            android:label="Broadcasts Unit Tests"/>
+</manifest>
\ No newline at end of file
diff --git a/tests/broadcasts/unit/AndroidTest.xml b/tests/broadcasts/unit/AndroidTest.xml
new file mode 100644
index 0000000..b91e4783
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidTest.xml
@@ -0,0 +1,29 @@
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Broadcasts tests">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="BroadcastUnitTests" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="BroadcastUnitTests.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.broadcasts.unit" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/broadcasts/unit/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING
new file mode 100644
index 0000000..0e824c5
--- /dev/null
+++ b/tests/broadcasts/unit/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+    "postsubmit": [
+        {
+            "name": "BroadcastUnitTests",
+            "options": [
+                {
+                    "exclude-annotation": "androidx.test.filters.FlakyTest"
+                },
+                {
+                    "exclude-annotation": "org.junit.Ignore"
+                }
+            ]
+        }
+    ]
+}
diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
new file mode 100644
index 0000000..b7c412d
--- /dev/null
+++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.app;
+
+import static android.content.Intent.ACTION_BATTERY_CHANGED;
+import static android.content.Intent.ACTION_DEVICE_STORAGE_LOW;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BroadcastStickyCacheTest {
+    @ClassRule
+    public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .mockStatic(SystemProperties.class)
+            .build();
+
+    private static final String PROP_KEY_BATTERY_CHANGED = BroadcastStickyCache.getKey(
+            ACTION_BATTERY_CHANGED);
+
+    private final TestSystemProps mTestSystemProps = new TestSystemProps();
+
+    @Before
+    public void setUp() {
+        doAnswer(invocation -> {
+            final String name = invocation.getArgument(0);
+            final long value = Long.parseLong(invocation.getArgument(1));
+            mTestSystemProps.add(name, value);
+            return null;
+        }).when(() -> SystemProperties.set(anyString(), anyString()));
+        doAnswer(invocation -> {
+            final String name = invocation.getArgument(0);
+            final TestSystemProps.Handle testHandle = mTestSystemProps.query(name);
+            if (testHandle == null) {
+                return null;
+            }
+            final SystemProperties.Handle handle = Mockito.mock(SystemProperties.Handle.class);
+            doAnswer(handleInvocation -> testHandle.getLong(-1)).when(handle).getLong(anyLong());
+            return handle;
+        }).when(() -> SystemProperties.find(anyString()));
+    }
+
+    @After
+    public void tearDown() {
+        mTestSystemProps.clear();
+        BroadcastStickyCache.clearForTest();
+    }
+
+    @Test
+    public void testUseCache_nullFilter() {
+        assertThat(BroadcastStickyCache.useCache(null)).isEqualTo(false);
+    }
+
+    @Test
+    public void testUseCache_noActions() {
+        final IntentFilter filter = new IntentFilter();
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+    }
+
+    @Test
+    public void testUseCache_multipleActions() {
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_DEVICE_STORAGE_LOW);
+        filter.addAction(ACTION_BATTERY_CHANGED);
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+    }
+
+    @Test
+    public void testUseCache_valueNotSet() {
+        final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+    }
+
+    @Test
+    public void testUseCache() {
+        final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+        final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+                .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        BroadcastStickyCache.add(filter, intent);
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+    }
+
+    @Test
+    public void testUseCache_versionMismatch() {
+        final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+        final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+                .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        BroadcastStickyCache.add(filter, intent);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+    }
+
+    @Test
+    public void testAdd() {
+        final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+        Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+                .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        BroadcastStickyCache.add(filter, intent);
+        assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+        Intent actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+        assertThat(actualIntent).isNotNull();
+        assertEquals(actualIntent, intent);
+
+        intent = new Intent(ACTION_BATTERY_CHANGED)
+                .putExtra(BatteryManager.EXTRA_LEVEL, 99);
+        BroadcastStickyCache.add(filter, intent);
+        actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+        assertThat(actualIntent).isNotNull();
+        assertEquals(actualIntent, intent);
+    }
+
+    @Test
+    public void testIncrementVersion_propExists() {
+        SystemProperties.set(PROP_KEY_BATTERY_CHANGED, String.valueOf(100));
+
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(101);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(102);
+    }
+
+    @Test
+    public void testIncrementVersion_propNotExists() {
+        // Verify that the property doesn't exist
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(1);
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+    }
+
+    @Test
+    public void testIncrementVersionIfExists_propExists() {
+        BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+        BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+        BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(3);
+    }
+
+    @Test
+    public void testIncrementVersionIfExists_propNotExists() {
+        // Verify that the property doesn't exist
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+        BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+        // Verify that property is not added as part of the querying.
+        BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+        assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+    }
+
+    private void assertEquals(Intent actualIntent, Intent expectedIntent) {
+        assertThat(actualIntent.getAction()).isEqualTo(expectedIntent.getAction());
+        assertEquals(actualIntent.getExtras(), expectedIntent.getExtras());
+    }
+
+    private void assertEquals(Bundle actualExtras, Bundle expectedExtras) {
+        assertWithMessage("Extras expected=%s, actual=%s", expectedExtras, actualExtras)
+                .that(actualExtras.kindofEquals(expectedExtras)).isTrue();
+    }
+
+    private static final class TestSystemProps {
+        @GuardedBy("mSysProps")
+        private final ArrayMap<String, Long> mSysProps = new ArrayMap<>();
+
+        public void add(String name, long value) {
+            synchronized (mSysProps) {
+                mSysProps.put(name, value);
+            }
+        }
+
+        public long get(String name, long defaultValue) {
+            synchronized (mSysProps) {
+                final int idx = mSysProps.indexOfKey(name);
+                return idx >= 0 ? mSysProps.valueAt(idx) : defaultValue;
+            }
+        }
+
+        public Handle query(String name) {
+            synchronized (mSysProps) {
+                return mSysProps.containsKey(name) ? new Handle(name) : null;
+            }
+        }
+
+        public void clear() {
+            synchronized (mSysProps) {
+                mSysProps.clear();
+            }
+        }
+
+        public class Handle {
+            private final String mName;
+
+            Handle(String name) {
+                mName = name;
+            }
+
+            public long getLong(long defaultValue) {
+                return get(mName, defaultValue);
+            }
+        }
+    }
+}