Merge "Adding tests for updated EmptyShadeView logic" into udc-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 3772960..df1b666 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4873,8 +4873,7 @@
                                 }
                             }
                             if (wakeupUids.size() > 0 && mBatteryStatsInternal != null) {
-                                mBatteryStatsInternal.noteCpuWakingActivity(
-                                        BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM, nowELAPSED,
+                                mBatteryStatsInternal.noteWakingAlarmBatch(nowELAPSED,
                                         wakeupUids.toArray());
                             }
                             deliverAlarmsLocked(triggerList, nowELAPSED);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 9bbc4a6..ae63816 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2075,6 +2075,14 @@
 
   public final class SoundTriggerManager {
     method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public static android.media.soundtrigger.SoundTriggerInstrumentation attachInstrumentation(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.GlobalCallback);
+    method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForModule(@NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties);
+    method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForTestModule();
+    method @NonNull public static java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties();
+    method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
+  }
+
+  public static class SoundTriggerManager.Model {
+    method @NonNull public android.hardware.soundtrigger.SoundTrigger.SoundModel getSoundModel();
   }
 
 }
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 08a1af4..3d4b6bf 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -209,10 +209,10 @@
  * The overlay will maintain the same relative position within the window bounds as the window
  * moves. The overlay will also maintain the same relative position within the window bounds if
  * the window is resized.
- * To attach an overlay to a window, use {@link attachAccessibilityOverlayToWindow}.
+ * To attach an overlay to a window, use {@link #attachAccessibilityOverlayToWindow}.
  * Attaching an overlay to the display means that the overlay is independent of the active
  * windows on that display.
- * To attach an overlay to a display, use {@link attachAccessibilityOverlayToDisplay}. </p>
+ * To attach an overlay to a display, use {@link #attachAccessibilityOverlayToDisplay}. </p>
  * <p> When positioning an overlay that is attached to a window, the service must use window
  * coordinates. In order to position an overlay on top of an existing UI element it is necessary
  * to know the bounds of that element in window coordinates. To find the bounds in window
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index 808f25e..d4a96b4 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -1018,7 +1018,7 @@
      *
      * @param motionEventSources A bit mask of {@link android.view.InputDevice} sources.
      * @see AccessibilityService#onMotionEvent
-     * @see MotionEventSources
+     * @see #MotionEventSources
      */
     public void setMotionEventSources(@MotionEventSources int motionEventSources) {
         mMotionEventSources = motionEventSources;
diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java
index bf69531..b5efb73 100644
--- a/core/java/android/app/LoadedApk.java
+++ b/core/java/android/app/LoadedApk.java
@@ -1668,10 +1668,13 @@
     static final class ReceiverDispatcher {
 
         final static class InnerReceiver extends IIntentReceiver.Stub {
+            final IApplicationThread mApplicationThread;
             final WeakReference<LoadedApk.ReceiverDispatcher> mDispatcher;
             final LoadedApk.ReceiverDispatcher mStrongRef;
 
-            InnerReceiver(LoadedApk.ReceiverDispatcher rd, boolean strong) {
+            InnerReceiver(IApplicationThread thread, LoadedApk.ReceiverDispatcher rd,
+                    boolean strong) {
+                mApplicationThread = thread;
                 mDispatcher = new WeakReference<LoadedApk.ReceiverDispatcher>(rd);
                 mStrongRef = strong ? rd : null;
             }
@@ -1718,7 +1721,8 @@
                         if (extras != null) {
                             extras.setAllowFds(false);
                         }
-                        mgr.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
+                        mgr.finishReceiver(mApplicationThread.asBinder(), resultCode, data,
+                                extras, false, intent.getFlags());
                     } catch (RemoteException e) {
                         throw e.rethrowFromSystemServer();
                     }
@@ -1825,7 +1829,7 @@
             }
 
             mAppThread = appThread;
-            mIIntentReceiver = new InnerReceiver(this, !registered);
+            mIIntentReceiver = new InnerReceiver(mAppThread, this, !registered);
             mReceiver = receiver;
             mContext = context;
             mActivityThread = activityThread;
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index 31c02b8..fa99b59 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -2085,7 +2085,8 @@
      *
      * @param uri The URI whose file is to be opened.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      *
      * @return Returns a new ParcelFileDescriptor which you can use to access
      * the file.
@@ -2147,7 +2148,8 @@
      *
      * @param uri The URI whose file is to be opened.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @param signal A signal to cancel the operation in progress, or
      *            {@code null} if none. For example, if you are downloading a
      *            file from the network to service a "rw" mode request, you
@@ -2208,7 +2210,8 @@
      *
      * @param uri The URI whose file is to be opened.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      *
      * @return Returns a new AssetFileDescriptor which you can use to access
      * the file.
@@ -2262,7 +2265,8 @@
      *
      * @param uri The URI whose file is to be opened.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @param signal A signal to cancel the operation in progress, or
      *            {@code null} if none. For example, if you are downloading a
      *            file from the network to service a "rw" mode request, you
@@ -2294,7 +2298,8 @@
      *
      * @param uri The URI to be opened.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      *
      * @return Returns a new ParcelFileDescriptor that can be used by the
      * client to access the file.
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index feca7a0..b2cd7e9 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -1536,7 +1536,8 @@
 
     /**
      * Synonym for {@link #openOutputStream(Uri, String)
-     * openOutputStream(uri, "w")}.
+     * openOutputStream(uri, "w")}. Please note the implementation of "w" is up to each
+     * Provider implementation and it may or may not truncate.
      *
      * @param uri The desired URI.
      * @return an OutputStream or {@code null} if the provider recently crashed.
@@ -1562,7 +1563,8 @@
      *
      * @param uri The desired URI.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @return an OutputStream or {@code null} if the provider recently crashed.
      * @throws FileNotFoundException if the provided URI could not be opened.
      * @see #openAssetFileDescriptor(Uri, String)
@@ -1619,7 +1621,8 @@
      *
      * @param uri The desired URI to open.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @return Returns a new ParcelFileDescriptor pointing to the file or {@code null} if the
      * provider recently crashed. You own this descriptor and are responsible for closing it
      * when done.
@@ -1662,7 +1665,8 @@
      *
      * @param uri The desired URI to open.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @param cancellationSignal A signal to cancel the operation in progress,
      *         or null if none. If the operation is canceled, then
      *         {@link OperationCanceledException} will be thrown.
@@ -1756,7 +1760,8 @@
      *
      * @param uri The desired URI to open.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note the exact implementation of these may differ for each
+     *             Provider implementation - for example, "w" may or may not truncate.
      * @return Returns a new ParcelFileDescriptor pointing to the file or {@code null} if the
      * provider recently crashed. You own this descriptor and are responsible for closing it
      * when done.
@@ -1810,7 +1815,8 @@
      *
      * @param uri The desired URI to open.
      * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
-     *             or "rwt". See{@link ParcelFileDescriptor#parseMode} for more details.
+     *             or "rwt". Please note "w" is write only and "wt" is write and truncate.
+     *             See{@link ParcelFileDescriptor#parseMode} for more details.
      * @param cancellationSignal A signal to cancel the operation in progress, or null if
      *            none. If the operation is canceled, then
      *            {@link OperationCanceledException} will be thrown.
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 2b73afc..c221d72 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -7613,7 +7613,7 @@
      * the device association is changed by the system.
      * <p>
      * The callback can be called when an app is moved to a different device and the {@code Context}
-     * is not explicily associated with a specific device.
+     * is not explicitly associated with a specific device.
      * </p>
      * <p> When an application receives a device id update callback, this Context is guaranteed to
      * also have an updated display ID(if any) and {@link Configuration}.
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index e0af913..0221296 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -44,6 +44,7 @@
 import android.os.RemoteException;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.Slog;
 import android.view.Surface;
 
@@ -127,6 +128,11 @@
         @Override // binder call
         public void onRemoved(Face face, int remaining) {
             mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget();
+            if (remaining == 0) {
+                Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                        Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0,
+                        UserHandle.USER_CURRENT);
+            }
         }
 
         @Override
@@ -342,6 +348,13 @@
             return;
         }
 
+        if (hardwareAuthToken == null) {
+            callback.onEnrollmentError(FACE_ERROR_UNABLE_TO_PROCESS,
+                    getErrorString(mContext, FACE_ERROR_UNABLE_TO_PROCESS,
+                    0 /* vendorCode */));
+            return;
+        }
+
         if (getEnrolledFaces(userId).size()
                 >= mContext.getResources().getInteger(R.integer.config_faceMaxTemplatesPerUser)) {
             callback.onEnrollmentError(FACE_ERROR_HW_UNAVAILABLE,
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index eb8136e..01977f6 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -712,6 +712,13 @@
             return;
         }
 
+        if (hardwareAuthToken == null) {
+            callback.onEnrollmentError(FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
+                    getErrorString(mContext, FINGERPRINT_ERROR_UNABLE_TO_PROCESS,
+                            0 /* vendorCode */));
+            return;
+        }
+
         if (mService != null) {
             try {
                 mEnrollmentCallback = callback;
@@ -832,7 +839,7 @@
     }
 
     /**
-     * Removes all face templates for the given user.
+     * Removes all fingerprint templates for the given user.
      * @hide
      */
     @RequiresPermission(MANAGE_FINGERPRINT)
diff --git a/core/java/android/hardware/usb/UsbPortStatus.java b/core/java/android/hardware/usb/UsbPortStatus.java
index e1662b8..b4fe3a2 100644
--- a/core/java/android/hardware/usb/UsbPortStatus.java
+++ b/core/java/android/hardware/usb/UsbPortStatus.java
@@ -588,10 +588,10 @@
      * Returns non compliant reasons, if any, for the connected
      * charger/cable/accessory/USB port.
      *
-     * @return array including {@link #NON_COMPLIANT_REASON_DEBUG_ACCESSORY},
-     *         {@link #NON_COMPLIANT_REASON_BC12},
-     *         {@link #NON_COMPLIANT_REASON_MISSING_RP},
-     *         or {@link #NON_COMPLIANT_REASON_TYPEC}
+     * @return array including {@link #COMPLIANCE_WARNING_OTHER},
+     *         {@link #COMPLIANCE_WARNING_DEBUG_ACCESSORY},
+     *         {@link #COMPLIANCE_WARNING_BC_1_2},
+     *         or {@link #COMPLIANCE_WARNING_MISSING_RP}
      */
     @CheckResult
     @NonNull
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index a52e3d49..2c31e32 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -213,6 +213,13 @@
     }
 
     /**
+     * Switch the system to use ANGLE as the default GLES driver.
+     */
+    public void toggleAngleAsSystemDriver(boolean enabled) {
+        nativeToggleAngleAsSystemDriver(enabled);
+    }
+
+    /**
      * Query to determine if the Game Mode has enabled ANGLE.
      */
     private boolean isAngleEnabledByGameMode(Context context, String packageName) {
@@ -992,6 +999,7 @@
             String appPackage, boolean angleIsSystemDriver, String legacyDriverName);
     private static native boolean getShouldUseAngle(String packageName);
     private static native boolean setInjectLayersPrSetDumpable();
+    private static native void nativeToggleAngleAsSystemDriver(boolean enabled);
 
     /**
      * Hint for GraphicsEnvironment that an activity is launching on the process.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2842cdb..2efb265 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10202,9 +10202,7 @@
          *
          * Face unlock re enroll.
          *  0 = No re enrollment.
-         *  1 = Re enrollment is suggested.
-         *  2 = Re enrollment is required after a set time period.
-         *  3 = Re enrollment is required immediately.
+         *  1 = Re enrollment is required.
          *
          * @hide
          */
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 9d3d70d..8d84e44 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -2738,7 +2738,12 @@
             engineWrapper.destroy();
         }
         mActiveEngines.clear();
-        mBackgroundThread.quitSafely();
+        if (mBackgroundThread != null) {
+            // onDestroy might be called without a previous onCreate if WallpaperService was
+            // instantiated manually. While this is a misuse of the API, some things break
+            // if here we don't take into consideration this scenario.
+            mBackgroundThread.quitSafely();
+        }
         Trace.endSection();
     }
 
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index baefd85..60529c7 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2188,7 +2188,7 @@
          */
         @NonNull
         public float[] getAlternativeRefreshRates() {
-            return mAlternativeRefreshRates;
+            return Arrays.copyOf(mAlternativeRefreshRates, mAlternativeRefreshRates.length);
         }
 
         /**
@@ -2197,7 +2197,7 @@
         @NonNull
         @HdrCapabilities.HdrType
         public int[] getSupportedHdrTypes() {
-            return mSupportedHdrTypes;
+            return Arrays.copyOf(mSupportedHdrTypes, mSupportedHdrTypes.length);
         }
 
         /**
@@ -2497,8 +2497,10 @@
          * @deprecated use {@link Display#getMode()}
          * and {@link Mode#getSupportedHdrTypes()} instead
          */
-        public @HdrType int[] getSupportedHdrTypes() {
-            return mSupportedHdrTypes;
+        @Deprecated
+        @HdrType
+        public int[] getSupportedHdrTypes() {
+            return Arrays.copyOf(mSupportedHdrTypes, mSupportedHdrTypes.length);
         }
         /**
          * Returns the desired content max luminance data in cd/m2 for this display.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 2b29e78..6bd9538 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -885,6 +885,16 @@
      */
     private SurfaceSyncGroup mActiveSurfaceSyncGroup;
 
+
+    private final Object mPreviousSyncSafeguardLock = new Object();
+
+    /**
+     * Wraps the TransactionCommitted callback for the previous SSG so it can be added to the next
+     * SSG if started before previous has completed.
+     */
+    @GuardedBy("mPreviousSyncSafeguardLock")
+    private SurfaceSyncGroup mPreviousSyncSafeguard;
+
     private static final Object sSyncProgressLock = new Object();
     // The count needs to be static since it's used to enable or disable RT animations which is
     // done at a global level per process. If any VRI syncs are in progress, we can't enable RT
@@ -11329,6 +11339,61 @@
         });
     }
 
+    /**
+     * This code will ensure that if multiple SurfaceSyncGroups are created for the same
+     * ViewRootImpl the SurfaceSyncGroups will maintain an order. The scenario that could occur
+     * is the following:
+     * <p>
+     * 1. SSG1 is created that includes the target VRI. There could be other VRIs in SSG1
+     * 2. The target VRI draws its frame and marks its own active SSG as ready, but SSG1 is still
+     *    waiting on other things in the SSG
+     * 3. Another SSG2 is created for the target VRI. The second frame renders and marks its own
+     *    second SSG as complete. SSG2 has nothing else to wait on, so it will apply at this point,
+     *    even though SSG1 has not finished.
+     * 4. Frame2 will get to SF first and Frame1 will later get to SF when SSG1 completes.
+     * <p>
+     * The code below ensures the SSGs that contains the VRI maintain an order. We create a new SSG
+     * that's a safeguard SSG. Its only job is to prevent the next active SSG from completing.
+     * The current active SSG for VRI will add a transaction committed callback and when that's
+     * invoked, it will mark the safeguard SSG as ready. If a new request to create a SSG comes
+     * in and the safeguard SSG is not null, it's added as part of the new active SSG. A new
+     * safeguard SSG is created to correspond to the new active SSG. This creates a chain to
+     * ensure the latter SSG always waits for the former SSG's transaction to get to SF.
+     */
+    private void safeguardOverlappingSyncs(SurfaceSyncGroup activeSurfaceSyncGroup) {
+        SurfaceSyncGroup safeguardSsg = new SurfaceSyncGroup("VRI-Safeguard");
+        // Always disable timeout on the safeguard sync
+        safeguardSsg.toggleTimeout(false /* enable */);
+        synchronized (mPreviousSyncSafeguardLock) {
+            if (mPreviousSyncSafeguard != null) {
+                activeSurfaceSyncGroup.add(mPreviousSyncSafeguard, null /* runnable */);
+                // Temporarily disable the timeout on the SSG that will contain the buffer. This
+                // is to ensure we don't timeout the active SSG before the previous one completes to
+                // ensure the order is maintained. The previous SSG has a timeout on its own SSG
+                // so it's guaranteed to complete.
+                activeSurfaceSyncGroup.toggleTimeout(false /* enable */);
+                mPreviousSyncSafeguard.addSyncCompleteCallback(mSimpleExecutor, () -> {
+                    // Once we receive that the previous sync guard has been invoked, we can re-add
+                    // the timeout on the active sync to ensure we eventually complete so it's not
+                    // stuck permanently.
+                    activeSurfaceSyncGroup.toggleTimeout(true /*enable */);
+                });
+            }
+            mPreviousSyncSafeguard = safeguardSsg;
+        }
+
+        Transaction t = new Transaction();
+        t.addTransactionCommittedListener(mSimpleExecutor, () -> {
+            safeguardSsg.markSyncReady();
+            synchronized (mPreviousSyncSafeguardLock) {
+                if (mPreviousSyncSafeguard == safeguardSsg) {
+                    mPreviousSyncSafeguard = null;
+                }
+            }
+        });
+        activeSurfaceSyncGroup.addTransaction(t);
+    }
+
     @Override
     public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() {
         boolean newSyncGroup = false;
@@ -11355,6 +11420,7 @@
                     mHandler.post(runnable);
                 }
             });
+            safeguardOverlappingSyncs(mActiveSurfaceSyncGroup);
             updateSyncInProgressCount(mActiveSurfaceSyncGroup);
             newSyncGroup = true;
         }
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index ddcb431..5b6df1c 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -467,7 +467,7 @@
      * implementation.
      * @hide
      */
-    int TRANSIT_FIRST_CUSTOM = 13;
+    int TRANSIT_FIRST_CUSTOM = 1000;
 
     /**
      * @hide
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index b3d5124..1840567 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -38,6 +38,7 @@
 import android.view.WindowManagerGlobal;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -63,7 +64,12 @@
     private static final int MAX_COUNT = 100;
 
     private static final AtomicInteger sCounter = new AtomicInteger(0);
-    private static final int TRANSACTION_READY_TIMEOUT = 1000 * Build.HW_TIMEOUT_MULTIPLIER;
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static final int TRANSACTION_READY_TIMEOUT = 1000 * Build.HW_TIMEOUT_MULTIPLIER;
 
     private static Supplier<Transaction> sTransactionFactory = Transaction::new;
 
@@ -123,6 +129,14 @@
     @GuardedBy("mLock")
     private boolean mTimeoutAdded;
 
+    /**
+     * Disable the timeout for this SSG so it will never be set until there's an explicit call to
+     * add a timeout.
+     */
+    @GuardedBy("mLock")
+    private boolean mTimeoutDisabled;
+
+
     private static boolean isLocalBinder(IBinder binder) {
         return !(binder instanceof BinderProxy);
     }
@@ -223,6 +237,10 @@
      */
     public void addSyncCompleteCallback(Executor executor, Runnable runnable) {
         synchronized (mLock) {
+            if (mFinished) {
+                executor.execute(runnable);
+                return;
+            }
             mSyncCompleteCallbacks.add(new Pair<>(executor, runnable));
         }
     }
@@ -768,6 +786,21 @@
         }
     }
 
+    /**
+     * @hide
+     */
+    public void toggleTimeout(boolean enable) {
+        synchronized (mLock) {
+            mTimeoutDisabled = !enable;
+            if (mTimeoutAdded && !enable) {
+                mHandler.removeCallbacksAndMessages(this);
+                mTimeoutAdded = false;
+            } else if (!mTimeoutAdded && enable) {
+                addTimeout();
+            }
+        }
+    }
+
     private void addTimeout() {
         synchronized (sHandlerThreadLock) {
             if (sHandlerThread == null) {
@@ -777,7 +810,7 @@
         }
 
         synchronized (mLock) {
-            if (mTimeoutAdded) {
+            if (mTimeoutAdded || mTimeoutDisabled) {
                 // We only need one timeout for the entire SurfaceSyncGroup since we just want to
                 // ensure it doesn't stay stuck forever.
                 return;
diff --git a/core/java/android/window/TaskConstants.java b/core/java/android/window/TaskConstants.java
index 3a04198..e18fd50 100644
--- a/core/java/android/window/TaskConstants.java
+++ b/core/java/android/window/TaskConstants.java
@@ -47,37 +47,31 @@
             -2 * TASK_CHILD_LAYER_REGION_SIZE;
 
     /**
-     * When a unresizable app is moved in the different configuration, a restart button appears
-     * allowing to adapt (~resize) app to the new configuration mocks.
+     * Compat UI components: reachability education, size compat restart
+     * button, letterbox education, restart dialog.
      * @hide
      */
-    public static final int TASK_CHILD_LAYER_SIZE_COMPAT_RESTART_BUTTON =
-            TASK_CHILD_LAYER_REGION_SIZE;
+    public static final int TASK_CHILD_LAYER_COMPAT_UI = TASK_CHILD_LAYER_REGION_SIZE;
 
-    /**
-     * Shown the first time an app is opened in size compat mode in landscape.
-     * @hide
-     */
-    public static final int TASK_CHILD_LAYER_LETTERBOX_EDUCATION = 2 * TASK_CHILD_LAYER_REGION_SIZE;
 
     /**
      * Captions, window frames and resize handlers around task windows.
      * @hide
      */
-    public static final int TASK_CHILD_LAYER_WINDOW_DECORATIONS = 3 * TASK_CHILD_LAYER_REGION_SIZE;
+    public static final int TASK_CHILD_LAYER_WINDOW_DECORATIONS = 2 * TASK_CHILD_LAYER_REGION_SIZE;
 
     /**
      * Overlays the task when going into PIP w/ gesture navigation.
      * @hide
      */
     public static final int TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY =
-            4 * TASK_CHILD_LAYER_REGION_SIZE;
+            3 * TASK_CHILD_LAYER_REGION_SIZE;
 
     /**
      * Allows other apps to add overlays on the task (i.e. game dashboard)
      * @hide
      */
-    public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 5 * TASK_CHILD_LAYER_REGION_SIZE;
+    public static final int TASK_CHILD_LAYER_TASK_OVERLAY = 4 * TASK_CHILD_LAYER_REGION_SIZE;
 
     /**
      * Z-orders of task child layers other than activities, task fragments and layers interleaved
@@ -87,8 +81,7 @@
     @IntDef({
             TASK_CHILD_LAYER_TASK_BACKGROUND,
             TASK_CHILD_LAYER_LETTERBOX_BACKGROUND,
-            TASK_CHILD_LAYER_SIZE_COMPAT_RESTART_BUTTON,
-            TASK_CHILD_LAYER_LETTERBOX_EDUCATION,
+            TASK_CHILD_LAYER_COMPAT_UI,
             TASK_CHILD_LAYER_WINDOW_DECORATIONS,
             TASK_CHILD_LAYER_RECENTS_ANIMATION_PIP_OVERLAY,
             TASK_CHILD_LAYER_TASK_OVERLAY
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 0f3eef7..628fc31 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -152,8 +152,14 @@
     /** The task became the top-most task even if it didn't change visibility. */
     public static final int FLAG_MOVED_TO_TOP = 1 << 20;
 
+    /**
+     * This transition must be the only transition when it starts (ie. it must wait for all other
+     * transition animations to finish).
+     */
+    public static final int FLAG_SYNC = 1 << 21;
+
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
-    public static final int FLAG_FIRST_CUSTOM = 1 << 21;
+    public static final int FLAG_FIRST_CUSTOM = 1 << 22;
 
     /** The change belongs to a window that won't contain activities. */
     public static final int FLAGS_IS_NON_APP_WINDOW =
@@ -183,12 +189,14 @@
             FLAG_NO_ANIMATION,
             FLAG_TASK_LAUNCHING_BEHIND,
             FLAG_MOVED_TO_TOP,
+            FLAG_SYNC,
             FLAG_FIRST_CUSTOM
     })
     public @interface ChangeFlags {}
 
     private final @TransitionType int mType;
-    private final @TransitionFlags int mFlags;
+    private @TransitionFlags int mFlags;
+    private int mTrack = 0;
     private final ArrayList<Change> mChanges = new ArrayList<>();
     private final ArrayList<Root> mRoots = new ArrayList<>();
 
@@ -210,6 +218,7 @@
         in.readTypedList(mRoots, Root.CREATOR);
         mOptions = in.readTypedObject(AnimationOptions.CREATOR);
         mDebugId = in.readInt();
+        mTrack = in.readInt();
     }
 
     @Override
@@ -221,6 +230,7 @@
         dest.writeTypedList(mRoots, flags);
         dest.writeTypedObject(mOptions, flags);
         dest.writeInt(mDebugId);
+        dest.writeInt(mTrack);
     }
 
     @NonNull
@@ -262,6 +272,10 @@
         return mType;
     }
 
+    public void setFlags(int flags) {
+        mFlags = flags;
+    }
+
     public int getFlags() {
         return mFlags;
     }
@@ -356,6 +370,16 @@
         return (mFlags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0;
     }
 
+    /** Gets which animation track this transition should run on. */
+    public int getTrack() {
+        return mTrack;
+    }
+
+    /** Sets which animation track this transition should run on. */
+    public void setTrack(int track) {
+        mTrack = track;
+    }
+
     /**
      * Set an arbitrary "debug" id for this info. This id will not be used for any "real work",
      * it is just for debugging and logging.
@@ -373,7 +397,8 @@
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("{id=").append(mDebugId).append(" t=").append(transitTypeToString(mType))
-                .append(" f=0x").append(Integer.toHexString(mFlags)).append(" r=[");
+                .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack)
+                .append(" r=[");
         for (int i = 0; i < mRoots.size(); ++i) {
             if (i > 0) {
                 sb.append(',');
@@ -461,6 +486,9 @@
         if ((flags & FLAG_TASK_LAUNCHING_BEHIND) != 0) {
             sb.append((sb.length() == 0 ? "" : "|") + "TASK_LAUNCHING_BEHIND");
         }
+        if ((flags & FLAG_SYNC) != 0) {
+            sb.append((sb.length() == 0 ? "" : "|") + "SYNC");
+        }
         if ((flags & FLAG_FIRST_CUSTOM) != 0) {
             sb.append(sb.length() == 0 ? "" : "|").append("FIRST_CUSTOM");
         }
@@ -532,6 +560,7 @@
      */
     public TransitionInfo localRemoteCopy() {
         final TransitionInfo out = new TransitionInfo(mType, mFlags);
+        out.mTrack = mTrack;
         out.mDebugId = mDebugId;
         for (int i = 0; i < mChanges.size(); ++i) {
             out.mChanges.add(mChanges.get(i).localRemoteCopy());
diff --git a/core/jni/android_os_GraphicsEnvironment.cpp b/core/jni/android_os_GraphicsEnvironment.cpp
index 78e2d31..d9152d6 100644
--- a/core/jni/android_os_GraphicsEnvironment.cpp
+++ b/core/jni/android_os_GraphicsEnvironment.cpp
@@ -122,6 +122,10 @@
     android::GraphicsEnv::getInstance().hintActivityLaunch();
 }
 
+void nativeToggleAngleAsSystemDriver_native(JNIEnv* env, jobject clazz, jboolean enabled) {
+    android::GraphicsEnv::getInstance().nativeToggleAngleAsSystemDriver(enabled);
+}
+
 const JNINativeMethod g_methods[] = {
         {"isDebuggable", "()Z", reinterpret_cast<void*>(isDebuggable_native)},
         {"setDriverPathAndSphalLibraries", "(Ljava/lang/String;Ljava/lang/String;)V",
@@ -143,6 +147,8 @@
         {"setDebugLayersGLES", "(Ljava/lang/String;)V",
          reinterpret_cast<void*>(setDebugLayersGLES_native)},
         {"hintActivityLaunch", "()V", reinterpret_cast<void*>(hintActivityLaunch_native)},
+        {"nativeToggleAngleAsSystemDriver", "(Z)V",
+         reinterpret_cast<void*>(nativeToggleAngleAsSystemDriver_native)},
 };
 
 const char* const kGraphicsEnvironmentName = "android/os/GraphicsEnvironment";
diff --git a/core/proto/android/server/windowmanagertransitiontrace.proto b/core/proto/android/server/windowmanagertransitiontrace.proto
index 25985eb..c3cbc91 100644
--- a/core/proto/android/server/windowmanagertransitiontrace.proto
+++ b/core/proto/android/server/windowmanagertransitiontrace.proto
@@ -55,12 +55,14 @@
   optional int64 finish_time_ns = 6; // consider aborted if not provided
   required int32 type = 7;
   repeated Target targets = 8;
+  optional int32 flags = 9;
 }
 
 message Target {
   required int32 mode = 1;
   required int32 layer_id = 2;
   optional int32 window_id = 3;  // Not dumped in always on tracing
+  optional int32 flags = 4;
 }
 
 message TransitionState {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f6c9fab..11fcd1e 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7629,6 +7629,13 @@
     <permission android:name="android.permission.LOG_FOREGROUND_RESOURCE_USE"
                 android:protectionLevel="signature|module" />
 
+    <!-- @hide Allows the settings app to access GPU service APIs".
+        <p>Not for use by third-party applications.
+        <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.ACCESS_GPU_SERVICE"
+                android:protectionLevel="signature" />
+
     <!-- @hide Allows an application to get type of any provider uri.
          <p>Not for use by third-party applications.
          <p>Protection level: signature
@@ -8217,6 +8224,14 @@
             </intent-filter>
         </service>
 
+        <service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncConnectionService"
+                 android:permission="android.permission.BIND_CONNECTION_SERVICE"
+                 android:exported="true">
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService"/>
+            </intent-filter>
+        </service>
+
         <provider
             android:name="com.android.server.textclassifier.IconsContentProvider"
             android:authorities="com.android.textclassifier.icons"
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 4f91e7a..fb0f3d4 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -71,6 +71,7 @@
 import com.android.internal.content.ReferrerIntent;
 
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -95,7 +96,8 @@
     // few sequence numbers the framework used to launch the test activity.
     private static final int BASE_SEQ = 10000;
 
-    private final ActivityTestRule<TestActivity> mActivityTestRule =
+    @Rule
+    public final ActivityTestRule<TestActivity> mActivityTestRule =
             new ActivityTestRule<>(TestActivity.class, true /* initialTouchMode */,
                     false /* launchActivity */);
 
diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
index 1075d44..07dec5d 100644
--- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
+++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java
@@ -17,6 +17,7 @@
 package android.hardware.face;
 
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE;
+import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -178,6 +179,18 @@
         verify(mEnrollmentCallback).onEnrollmentError(eq(FACE_ERROR_HW_UNAVAILABLE), anyString());
     }
 
+    @Test
+    public void enrollment_errorWhenHardwareAuthTokenIsNull() throws RemoteException {
+        initializeProperties();
+        mFaceManager.enroll(USER_ID, null,
+                new CancellationSignal(), mEnrollmentCallback, null /* disabledFeatures */);
+
+        verify(mEnrollmentCallback).onEnrollmentError(eq(FACE_ERROR_UNABLE_TO_PROCESS),
+                anyString());
+        verify(mService, never()).enroll(eq(USER_ID), any(), any(),
+                any(), anyString(), any(), any(), anyBoolean());
+    }
+
     private void initializeProperties() throws RemoteException {
         verify(mService).addAuthenticatorsRegisteredCallback(mCaptor.capture());
 
diff --git a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java
index 5058065..625e2e3 100644
--- a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java
+++ b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java
@@ -17,12 +17,14 @@
 package android.hardware.fingerprint;
 
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE;
+import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -70,6 +72,8 @@
     private IFingerprintService mService;
     @Mock
     private FingerprintManager.AuthenticationCallback mAuthCallback;
+    @Mock
+    private FingerprintManager.EnrollmentCallback mEnrollCallback;
 
     @Captor
     private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mCaptor;
@@ -149,4 +153,17 @@
 
         verify(mAuthCallback).onAuthenticationError(eq(FINGERPRINT_ERROR_HW_UNAVAILABLE), any());
     }
+
+    @Test
+    public void enrollment_errorWhenHardwareAuthTokenIsNull() throws RemoteException {
+        verify(mService).addAuthenticatorsRegisteredCallback(mCaptor.capture());
+
+        mCaptor.getValue().onAllAuthenticatorsRegistered(mProps);
+        mFingerprintManager.enroll(null, new CancellationSignal(), USER_ID,
+                mEnrollCallback, FingerprintManager.ENROLL_ENROLL);
+
+        verify(mEnrollCallback).onEnrollmentError(eq(FINGERPRINT_ERROR_UNABLE_TO_PROCESS),
+                anyString());
+        verify(mService, never()).enroll(any(), any(), anyInt(), any(), anyString(), anyInt());
+    }
 }
diff --git a/core/tests/mockingcoretests/src/android/view/DisplayTest.java b/core/tests/mockingcoretests/src/android/view/DisplayTest.java
index 4a12bb3..5e617fd 100644
--- a/core/tests/mockingcoretests/src/android/view/DisplayTest.java
+++ b/core/tests/mockingcoretests/src/android/view/DisplayTest.java
@@ -460,6 +460,36 @@
         assertArrayEquals(sortedHdrTypes, displayMode.getSupportedHdrTypes());
     }
 
+    @Test
+    public void testGetSupportedHdrTypesReturnsCopy() {
+        int[] hdrTypes = new int[]{1, 2, 3};
+        Display.Mode displayMode = new Display.Mode(0, 0, 0, 0, new float[0], hdrTypes);
+
+        int[] hdrTypesCopy = displayMode.getSupportedHdrTypes();
+        hdrTypesCopy[0] = 0;
+        assertArrayEquals(hdrTypes, displayMode.getSupportedHdrTypes());
+    }
+
+    @Test
+    public void testGetAlternativeRefreshRatesReturnsCopy() {
+        float[] alternativeRates = new float[]{1.0f, 2.0f};
+        Display.Mode displayMode = new Display.Mode(0, 0, 0, 0, alternativeRates, new int[0]);
+
+        float[] alternativeRatesCopy = displayMode.getAlternativeRefreshRates();
+        alternativeRatesCopy[0] = 0.0f;
+        assertArrayEquals(alternativeRates, displayMode.getAlternativeRefreshRates(), 0.0f);
+    }
+
+    @Test
+    public void testHdrCapabilitiesGetSupportedHdrTypesReturnsCopy() {
+        int[] hdrTypes = new int[]{1, 2, 3};
+        Display.HdrCapabilities hdrCapabilities = new Display.HdrCapabilities(hdrTypes, 0, 0, 0);
+
+        int[] hdrTypesCopy = hdrCapabilities.getSupportedHdrTypes();
+        hdrTypesCopy[0] = 0;
+        assertArrayEquals(hdrTypes, hdrCapabilities.getSupportedHdrTypes());
+    }
+
     // Given rotated display dimensions, calculate the letterboxed app bounds.
     private static Rect buildAppBounds(int displayWidth, int displayHeight) {
         final int midWidth = displayWidth / 2;
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 7434cb0..549ac58 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3805,6 +3805,12 @@
       "group": "WM_DEBUG_APP_TRANSITIONS_ANIM",
       "at": "com\/android\/server\/wm\/AppTransitionController.java"
     },
+    "1463355909": {
+      "message": "Queueing legacy sync-set: %s",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
     "1469310004": {
       "message": "          SKIP: common mode mismatch. was %s",
       "level": "VERBOSE",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 66f27f5..a184dff 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -39,7 +39,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -167,14 +166,13 @@
     }
 
     @Override
-    protected void onListenersChanged(
-            @NonNull Set<Consumer<List<CommonFoldingFeature>>> callbacks) {
-        super.onListenersChanged(callbacks);
-        if (callbacks.isEmpty()) {
+    protected void onListenersChanged() {
+        super.onListenersChanged();
+        if (hasListeners()) {
+            mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
+        } else {
             mCurrentDeviceState = INVALID_DEVICE_STATE;
             mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
-        } else {
-            mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
         }
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
index 7906342..8906e6d 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/RawFoldingFeatureProducer.java
@@ -31,7 +31,6 @@
 import com.android.internal.R;
 
 import java.util.Optional;
-import java.util.Set;
 import java.util.function.Consumer;
 
 /**
@@ -86,11 +85,11 @@
     }
 
     @Override
-    protected void onListenersChanged(Set<Consumer<String>> callbacks) {
-        if (callbacks.isEmpty()) {
-            unregisterObserversIfNeeded();
-        } else {
+    protected void onListenersChanged() {
+        if (hasListeners()) {
             registerObserversIfNeeded();
+        } else {
+            unregisterObserversIfNeeded();
         }
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index c54e597..abf2301 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -414,8 +414,7 @@
             return WindowAreaComponent.STATUS_UNAVAILABLE;
         }
 
-        if (mRearDisplaySessionStatus == WindowAreaComponent.SESSION_STATE_ACTIVE
-                || isRearDisplayActive()) {
+        if (isRearDisplayActive()) {
             return WindowAreaComponent.STATUS_ACTIVE;
         }
 
@@ -537,7 +536,6 @@
                 if (request.equals(mRearDisplayStateRequest)) {
                     mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_ACTIVE;
                     mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
-                    updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
                 }
             }
         }
@@ -550,7 +548,6 @@
                 }
                 mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
                 mRearDisplaySessionCallback.accept(mRearDisplaySessionStatus);
-                updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
             }
         }
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
index 46c925a..de52f09 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/util/BaseDataProducer.java
@@ -51,10 +51,10 @@
     public final void addDataChangedCallback(@NonNull Consumer<T> callback) {
         synchronized (mLock) {
             mCallbacks.add(callback);
-            Optional<T> currentData = getCurrentData();
-            currentData.ifPresent(callback);
-            onListenersChanged(mCallbacks);
         }
+        Optional<T> currentData = getCurrentData();
+        currentData.ifPresent(callback);
+        onListenersChanged();
     }
 
     /**
@@ -67,11 +67,22 @@
     public final void removeDataChangedCallback(@NonNull Consumer<T> callback) {
         synchronized (mLock) {
             mCallbacks.remove(callback);
-            onListenersChanged(mCallbacks);
+        }
+        onListenersChanged();
+    }
+
+    /**
+     * Returns {@code true} if there are any registered callbacks {@code false} if there are no
+     * registered callbacks.
+     */
+    // TODO(b/278132889) Improve the structure of BaseDataProdcuer while avoiding known issues.
+    public final boolean hasListeners() {
+        synchronized (mLock) {
+            return !mCallbacks.isEmpty();
         }
     }
 
-    protected void onListenersChanged(Set<Consumer<T>> callbacks) {}
+    protected void onListenersChanged() {}
 
     /**
      * @return the current data if available and {@code Optional.empty()} otherwise.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 1b20f67..60111aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2922,14 +2922,15 @@
         final float targetX = isLtr
                 ? mTempRect.left - margin
                 : mTempRect.right + margin - mManageMenu.getWidth();
-        final float targetY = mTempRect.bottom - mManageMenu.getHeight();
+        final float menuHeight = getVisibleManageMenuHeight();
+        final float targetY = mTempRect.bottom - menuHeight;
 
         final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f;
         if (show) {
             mManageMenu.setScaleX(0.5f);
             mManageMenu.setScaleY(0.5f);
             mManageMenu.setTranslationX(targetX - xOffsetForAnimation);
-            mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4f);
+            mManageMenu.setTranslationY(targetY + menuHeight / 4f);
             mManageMenu.setAlpha(0f);
 
             PhysicsAnimator.getInstance(mManageMenu)
@@ -2955,7 +2956,7 @@
                     .spring(DynamicAnimation.SCALE_X, 0.5f)
                     .spring(DynamicAnimation.SCALE_Y, 0.5f)
                     .spring(DynamicAnimation.TRANSLATION_X, targetX - xOffsetForAnimation)
-                    .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4f)
+                    .spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f)
                     .withEndActions(() -> {
                         mManageMenu.setVisibility(View.INVISIBLE);
                         if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
@@ -3272,6 +3273,24 @@
     }
 
     /**
+     * Menu height calculated for animation
+     * It takes into account view visibility to get the correct total height
+     */
+    private float getVisibleManageMenuHeight() {
+        float menuHeight = 0;
+
+        for (int i = 0; i < mManageMenu.getChildCount(); i++) {
+            View subview = mManageMenu.getChildAt(i);
+
+            if (subview.getVisibility() == VISIBLE) {
+                menuHeight += subview.getHeight();
+            }
+        }
+
+        return menuHeight;
+    }
+
+    /**
      * @return the normalized x-axis position of the bubble stack rounded to 4 decimal places.
      */
     public float getNormalizedXPosition() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 170c0ee..6592292 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -20,6 +20,7 @@
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.app.TaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -46,11 +47,6 @@
  */
 class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
 
-    /**
-     * The Compat UI should be below the Letterbox Education.
-     */
-    private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1;
-
     private final CompatUICallback mCallback;
 
     private final CompatUIConfiguration mCompatUIConfiguration;
@@ -92,7 +88,7 @@
 
     @Override
     protected int getZOrder() {
-        return Z_ORDER;
+        return TASK_CHILD_LAYER_COMPAT_UI + 1;
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
index 0c21c8c..959c50d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/LetterboxEduWindowManager.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.compatui;
 
 import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -46,12 +47,6 @@
  */
 class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract {
 
-    /**
-     * The Letterbox Education should be the topmost child of the Task in case there can be more
-     * than one child.
-     */
-    public static final int Z_ORDER = Integer.MAX_VALUE;
-
     private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
 
     private final Transitions mTransitions;
@@ -118,7 +113,7 @@
 
     @Override
     protected int getZOrder() {
-        return Z_ORDER;
+        return TASK_CHILD_LAYER_COMPAT_UI + 2;
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
index b6e396d..a18ab91 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
@@ -18,6 +18,7 @@
 
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -41,11 +42,6 @@
  */
 class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
 
-    /**
-     * The Compat UI should be below the Letterbox Education.
-     */
-    private static final int Z_ORDER = LetterboxEduWindowManager.Z_ORDER - 1;
-
     // The time to wait before hiding the education
     private static final long DISAPPEAR_DELAY_MS = 4000L;
 
@@ -102,7 +98,7 @@
 
     @Override
     protected int getZOrder() {
-        return Z_ORDER;
+        return TASK_CHILD_LAYER_COMPAT_UI + 1;
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
index aab123a..51e5141 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/RestartDialogWindowManager.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.compatui;
 
 import static android.provider.Settings.Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -47,12 +48,6 @@
  */
 class RestartDialogWindowManager extends CompatUIWindowManagerAbstract {
 
-    /**
-     * The restart dialog should be the topmost child of the Task in case there can be more
-     * than one child.
-     */
-    private static final int Z_ORDER = Integer.MAX_VALUE;
-
     private final DialogAnimationController<RestartDialogLayout> mAnimationController;
 
     private final Transitions mTransitions;
@@ -112,7 +107,7 @@
 
     @Override
     protected int getZOrder() {
-        return Z_ORDER;
+        return TASK_CHILD_LAYER_COMPAT_UI + 2;
     }
 
     @Override
@@ -170,10 +165,10 @@
 
         final Rect taskBounds = getTaskBounds();
         final Rect taskStableBounds = getTaskStableBounds();
-
-        marginParams.topMargin = taskStableBounds.top - taskBounds.top + mDialogVerticalMargin;
-        marginParams.bottomMargin =
-                taskBounds.bottom - taskStableBounds.bottom + mDialogVerticalMargin;
+        // only update margins based on taskbar insets
+        marginParams.topMargin = mDialogVerticalMargin;
+        marginParams.bottomMargin = taskBounds.bottom - taskStableBounds.bottom
+                + mDialogVerticalMargin;
         dialogContainer.setLayoutParams(marginParams);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index b6216b3..566c130 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1690,8 +1690,17 @@
             // Similar to auto-enter-pip transition, we use content overlay when there is no
             // source rect hint to enter PiP use bounds animation.
             if (sourceHintRect == null) {
+                // We use content overlay when there is no source rect hint to enter PiP use bounds
+                // animation.
+                // TODO(b/272819817): cleanup the null-check and extra logging.
+                final boolean hasTopActivityInfo = mTaskInfo.topActivityInfo != null;
+                if (!hasTopActivityInfo) {
+                    ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                            "%s: TaskInfo.topActivityInfo is null", TAG);
+                }
                 if (SystemProperties.getBoolean(
-                        "persist.wm.debug.enable_pip_app_icon_overlay", true)) {
+                        "persist.wm.debug.enable_pip_app_icon_overlay", true)
+                        && hasTopActivityInfo) {
                     animator.setAppIconContentOverlay(
                             mContext, currentBounds, mTaskInfo.topActivityInfo,
                             mPipBoundsState.getLauncherState().getAppIconSizePx());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index c4a0e9c..ef5e501 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -33,7 +33,7 @@
     WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM_SHELL),
     WM_SHELL_RECENTS_TRANSITION(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
-            Consts.TAG_WM_SHELL),
+            "ShellRecents"),
     WM_SHELL_DRAG_AND_DROP(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM_SHELL),
     WM_SHELL_STARTING_WINDOW(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index b554872..4f362d4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -100,6 +100,7 @@
             IApplicationThread appThread, IRecentsAnimationRunner listener) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                 "RecentsTransitionHandler.startRecentsTransition");
+
         // only care about latest one.
         mAnimApp = appThread;
         WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -169,7 +170,11 @@
             SurfaceControl.Transaction t, IBinder mergeTarget,
             Transitions.TransitionFinishCallback finishCallback) {
         final int targetIdx = findController(mergeTarget);
-        if (targetIdx < 0) return;
+        if (targetIdx < 0) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "RecentsTransitionHandler.mergeAnimation: no controller found");
+            return;
+        }
         final RecentsController controller = mControllers.get(targetIdx);
         controller.merge(info, t, finishCallback);
     }
@@ -177,13 +182,20 @@
     @Override
     public void onTransitionConsumed(IBinder transition, boolean aborted,
             SurfaceControl.Transaction finishTransaction) {
-        final int idx = findController(transition);
-        if (idx < 0) return;
-        mControllers.get(idx).cancel("onTransitionConsumed");
+        // Only one recents transition can be handled at a time, but currently the first transition
+        // will trigger a no-op in the second transition which holds the active recents animation
+        // runner on the launcher side.  For now, cancel all existing animations to ensure we
+        // don't get into a broken state with an orphaned animation runner, and later we can try to
+        // merge the latest transition into the currently running one
+        for (int i = mControllers.size() - 1; i >= 0; i--) {
+            mControllers.get(i).cancel("onTransitionConsumed");
+        }
     }
 
     /** There is only one of these and it gets reset on finish. */
     private class RecentsController extends IRecentsAnimationController.Stub {
+        private final int mInstanceId;
+
         private IRecentsAnimationRunner mListener;
         private IBinder.DeathRecipient mDeathHandler;
         private Transitions.TransitionFinishCallback mFinishCB = null;
@@ -223,10 +235,11 @@
         private int mState = STATE_NORMAL;
 
         RecentsController(IRecentsAnimationRunner listener) {
+            mInstanceId = System.identityHashCode(this);
             mListener = listener;
             mDeathHandler = () -> {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.DeathRecipient: binder died");
+                        "[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
                 finish(mWillFinishToHome, false /* leaveHint */);
             };
             try {
@@ -239,7 +252,7 @@
 
         void setTransition(IBinder transition) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                    "RecentsController.setTransition: id=%s", transition);
+                    "[%d] RecentsController.setTransition: id=%s", mInstanceId, transition);
             mTransition = transition;
         }
 
@@ -251,11 +264,13 @@
 
         void cancel(boolean toHome, String reason) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                    "RecentsController.cancel: toHome=%b reason=%s", toHome, reason);
+                    "[%d] RecentsController.cancel: toHome=%b reason=%s",
+                    mInstanceId, toHome, reason);
             if (mListener != null) {
                 try {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                            "RecentsController.cancel: calling onAnimationCanceled");
+                            "[%d] RecentsController.cancel: calling onAnimationCanceled",
+                            mInstanceId);
                     mListener.onAnimationCanceled(null, null);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error canceling recents animation", e);
@@ -290,7 +305,8 @@
             }
             try {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.cancel: calling onAnimationCanceled with snapshots");
+                        "[%d] RecentsController.cancel: calling onAnimationCanceled with snapshots",
+                        mInstanceId);
                 mListener.onAnimationCanceled(taskIds, snapshots);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error canceling recents animation", e);
@@ -300,7 +316,8 @@
         }
 
         void cleanUp() {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.cleanup");
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.cleanup", mInstanceId);
             if (mListener != null && mDeathHandler != null) {
                 mListener.asBinder().unlinkToDeath(mDeathHandler, 0 /* flags */);
                 mDeathHandler = null;
@@ -324,7 +341,8 @@
 
         boolean start(TransitionInfo info, SurfaceControl.Transaction t,
                 SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.start");
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.start", mInstanceId);
             if (mListener == null || mTransition == null) {
                 cleanUp();
                 return false;
@@ -409,7 +427,7 @@
             t.apply();
             try {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.start: calling onAnimationStart");
+                        "[%d] RecentsController.start: calling onAnimationStart", mInstanceId);
                 mListener.onAnimationStart(this,
                         apps.toArray(new RemoteAnimationTarget[apps.size()]),
                         wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
@@ -426,18 +444,20 @@
                 Transitions.TransitionFinishCallback finishCallback) {
             if (mFinishCB == null) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.merge: skip, no finish callback");
+                        "[%d] RecentsController.merge: skip, no finish callback",
+                        mInstanceId);
                 // This was no-op'd (likely a repeated start) and we've already sent finish.
                 return;
             }
             if (info.getType() == TRANSIT_SLEEP) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.merge: transit_sleep");
+                        "[%d] RecentsController.merge: transit_sleep", mInstanceId);
                 // A sleep event means we need to stop animations immediately, so cancel here.
                 cancel("transit_sleep");
                 return;
             }
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.merge");
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                    "[%d] RecentsController.merge", mInstanceId);
             ArrayList<TransitionInfo.Change> openingTasks = null;
             ArrayList<TransitionInfo.Change> closingTasks = null;
             mOpeningSeparateHome = false;
@@ -595,7 +615,7 @@
             if (appearedTargets == null) return;
             try {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.merge: calling onTasksAppeared");
+                        "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId);
                 mListener.onTasksAppeared(appearedTargets);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
@@ -620,7 +640,7 @@
         public TaskSnapshot screenshotTask(int taskId) {
             try {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.screenshotTask: taskId=%d", taskId);
+                        "[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
                 return ActivityTaskManager.getService().takeTaskSnapshot(taskId);
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to screenshot task", e);
@@ -643,7 +663,8 @@
                 // animation finished.
                 try {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                            "RecentsController.setInputConsumerEnabled: set focus to recents");
+                            "[%d] RecentsController.setInputConsumerEnabled: set focus to recents",
+                            mInstanceId);
                     ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Failed to set focused task", e);
@@ -659,7 +680,8 @@
         public void setFinishTaskTransaction(int taskId,
                 PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                    "RecentsController.setFinishTaskTransaction: taskId=%d", taskId);
+                    "[%d] RecentsController.setFinishTaskTransaction: taskId=%d",
+                    mInstanceId, taskId);
             mExecutor.execute(() -> {
                 if (mFinishCB == null) return;
                 mPipTransaction = finishTransaction;
@@ -678,8 +700,8 @@
                 return;
             }
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                    "RecentsController.finishInner: toHome=%b userLeaveHint=%b willFinishToHome=%b",
-                    toHome, sendUserLeaveHint, mWillFinishToHome);
+                "[%d] RecentsController.finishInner: toHome=%b userLeave=%b willFinishToHome=%b",
+                    mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome);
             final Transitions.TransitionFinishCallback finishCB = mFinishCB;
             mFinishCB = null;
 
@@ -781,7 +803,7 @@
         @Override
         public void detachNavigationBarFromApp(boolean moveHomeToTop) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                    "RecentsController.detachNavigationBarFromApp");
+                    "[%d] RecentsController.detachNavigationBarFromApp", mInstanceId);
             mExecutor.execute(() -> {
                 if (mTransition == null) return;
                 try {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 5a92f78..7c729a4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -235,6 +235,7 @@
     private TransitionInfo subCopy(@NonNull TransitionInfo info,
             @WindowManager.TransitionType int newType, boolean withChanges) {
         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
+        out.setTrack(info.getTrack());
         out.setDebugId(info.getDebugId());
         if (withChanges) {
             for (int i = 0; i < info.getChanges().size(); ++i) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index bcc37ba..0f4645c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -122,14 +122,14 @@
                     ? R.styleable.WindowAnimation_taskToFrontEnterAnimation
                     : R.styleable.WindowAnimation_taskToFrontExitAnimation;
         } else if (type == TRANSIT_CLOSE) {
-            if (isTask) {
+            if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
+                translucent = true;
+            }
+            if (isTask && !translucent) {
                 animAttr = enter
                         ? R.styleable.WindowAnimation_taskCloseEnterAnimation
                         : R.styleable.WindowAnimation_taskCloseExitAnimation;
             } else {
-                if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) {
-                    translucent = true;
-                }
                 animAttr = enter
                         ? R.styleable.WindowAnimation_activityCloseEnterAnimation
                         : R.styleable.WindowAnimation_activityCloseExitAnimation;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 08b0bf7..5c8791e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -90,14 +90,21 @@
  * Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> |
  *                                                            --merge--> MERGED --^
  *
- * At the moment, only one transition can be animating at a time. While a transition is animating,
- * transitions will be queued in the "ready" state for their turn. At the same time, whenever a
- * transition makes it to the head of the "ready" queue, it will attempt to merge to with the
- * "active" transition. If the merge succeeds, it will be moved to the "active" transition's
- * "merged" and then the next "ready" transition can attempt to merge.
+ * The READY and beyond lifecycle is managed per "track". Within a track, all the animations are
+ * serialized as described; however, multiple tracks can play simultaneously. This implies that,
+ * within a track, only one transition can be animating ("active") at a time.
  *
- * Once the "active" transition animation is finished, it will be removed from the "active" list
- * and then the next "ready" transition can play.
+ * While a transition is animating in a track, transitions dispatched to the track will be queued
+ * in the "ready" state for their turn. At the same time, whenever a transition makes it to the
+ * head of the "ready" queue, it will attempt to merge to with the "active" transition. If the
+ * merge succeeds, it will be moved to the "active" transition's "merged" list and then the next
+ * "ready" transition can attempt to merge. Once the "active" transition animation is finished,
+ * the next "ready" transition can play.
+ *
+ * Track assignments are expected to be provided by WMCore and this generally tries to maintain
+ * the same assignments. If, however, WMCore decides that a transition conflicts with >1 active
+ * track, it will be marked as SYNC. This means that all currently active tracks must be flushed
+ * before the SYNC transition can play.
  */
 public class Transitions implements RemoteCallable<Transitions> {
     static final String TAG = "ShellTransitions";
@@ -172,12 +179,15 @@
     private float mTransitionAnimationScaleSetting = 1.0f;
 
     /**
-     * How much time we allow for an animation to finish itself on sleep. If it takes longer, we
+     * How much time we allow for an animation to finish itself on sync. If it takes longer, we
      * will force-finish it (on this end) which may leave it in a bad state but won't hang the
      * device. This needs to be pretty small because it is an allowance for each queued animation,
      * however it can't be too small since there is some potential IPC involved.
      */
-    private static final int SLEEP_ALLOWANCE_MS = 120;
+    private static final int SYNC_ALLOWANCE_MS = 120;
+
+    /** For testing only. Disables the force-finish timeout on sync. */
+    private boolean mDisableForceSync = false;
 
     private static final class ActiveTransition {
         IBinder mToken;
@@ -190,23 +200,45 @@
         /** Ordered list of transitions which have been merged into this one. */
         private ArrayList<ActiveTransition> mMerged;
 
+        boolean isSync() {
+            return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0;
+        }
+
+        int getTrack() {
+            return mInfo != null ? mInfo.getTrack() : -1;
+        }
+
         @Override
         public String toString() {
             if (mInfo != null && mInfo.getDebugId() >= 0) {
-                return "(#" + mInfo.getDebugId() + ")" + mToken;
+                return "(#" + mInfo.getDebugId() + ")" + mToken + "@" + getTrack();
             }
-            return mToken.toString();
+            return mToken.toString() + "@" + getTrack();
+        }
+    }
+
+    private static class Track {
+        /** Keeps track of transitions which are ready to play but still waiting for their turn. */
+        final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>();
+
+        /** The currently playing transition in this track. */
+        ActiveTransition mActiveTransition = null;
+
+        boolean isIdle() {
+            return mActiveTransition == null && mReadyTransitions.isEmpty();
         }
     }
 
     /** Keeps track of transitions which have been started, but aren't ready yet. */
     private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>();
 
-    /** Keeps track of transitions which are ready to play but still waiting for their turn. */
-    private final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>();
+    /**
+     * Transitions which are ready to play, but haven't been sent to a track yet because a sync
+     * is ongoing.
+     */
+    private final ArrayList<ActiveTransition> mReadyDuringSync = new ArrayList<>();
 
-    /** Keeps track of currently playing transitions. For now, there can only be 1 max. */
-    private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
+    private final ArrayList<Track> mTracks = new ArrayList<>();
 
     public Transitions(@NonNull Context context,
             @NonNull ShellInit shellInit,
@@ -374,14 +406,17 @@
      * will be executed when the last active transition is finished.
      */
     public void runOnIdle(Runnable runnable) {
-        if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()
-                && mReadyTransitions.isEmpty()) {
+        if (isIdle()) {
             runnable.run();
         } else {
             mRunWhenIdleQueue.add(runnable);
         }
     }
 
+    void setDisableForceSyncForTest(boolean disable) {
+        mDisableForceSync = disable;
+    }
+
     /**
      * Sets up visibility/alpha/transforms to resemble the starting state of an animation.
      */
@@ -542,6 +577,13 @@
         return true;
     }
 
+    private Track getOrCreateTrack(int trackId) {
+        while (trackId >= mTracks.size()) {
+            mTracks.add(new Track());
+        }
+        return mTracks.get(trackId);
+    }
+
     @VisibleForTesting
     void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
@@ -554,29 +596,58 @@
                     + Arrays.toString(mPendingTransitions.stream().map(
                             activeTransition -> activeTransition.mToken).toArray()));
         }
-        if (activeIdx > 0) {
-            Log.e(TAG, "Transition became ready out-of-order " + mPendingTransitions.get(activeIdx)
-                    + ". Expected order: " + Arrays.toString(mPendingTransitions.stream().map(
-                            activeTransition -> activeTransition.mToken).toArray()));
-        }
         // Move from pending to ready
         final ActiveTransition active = mPendingTransitions.remove(activeIdx);
-        mReadyTransitions.add(active);
         active.mInfo = info;
         active.mStartT = t;
         active.mFinishT = finishT;
+        if (activeIdx > 0) {
+            Log.i(TAG, "Transition might be ready out-of-order " + activeIdx + " for " + active
+                    + ". This is ok if it's on a different track.");
+        }
+        if (!mReadyDuringSync.isEmpty()) {
+            mReadyDuringSync.add(active);
+        } else {
+            dispatchReady(active);
+        }
+    }
 
-        for (int i = 0; i < mObservers.size(); ++i) {
-            mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
+    /**
+     * Returns true if dispatching succeeded, otherwise false. Dispatching can fail if it is
+     * blocked by a sync or sleep.
+     */
+    boolean dispatchReady(ActiveTransition active) {
+        final TransitionInfo info = active.mInfo;
+
+        if (info.getType() == TRANSIT_SLEEP || active.isSync()) {
+            // Adding to *front*! If we are here, it means that it was pulled off the front
+            // so we are just putting it back; or, it is the first one so it doesn't matter.
+            mReadyDuringSync.add(0, active);
+            boolean hadPreceding = false;
+            // Now flush all the tracks.
+            for (int i = 0; i < mTracks.size(); ++i) {
+                final Track tr = mTracks.get(i);
+                if (tr.isIdle()) continue;
+                hadPreceding = true;
+                // Sleep starts a process of forcing all prior transitions to finish immediately
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                        "Start finish-for-sync track %d", i);
+                finishForSync(i, null /* forceFinish */);
+            }
+            if (hadPreceding) {
+                return false;
+            }
+            // Actually able to process the sleep now, so re-remove it from the queue and continue
+            // the normal flow.
+            mReadyDuringSync.remove(active);
         }
 
-        if (info.getType() == TRANSIT_SLEEP) {
-            if (activeIdx > 0 || !mActiveTransitions.isEmpty() || mReadyTransitions.size() > 1) {
-                // Sleep starts a process of forcing all prior transitions to finish immediately
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sleep");
-                finishForSleep(null /* forceFinish */);
-                return;
-            }
+        final Track track = getOrCreateTrack(info.getTrack());
+        track.mReadyTransitions.add(active);
+
+        for (int i = 0; i < mObservers.size(); ++i) {
+            mObservers.get(i).onTransitionReady(
+                    active.mToken, info, active.mStartT, active.mFinishT);
         }
 
         if (info.getRootCount() == 0 && !alwaysReportToKeyguard(info)) {
@@ -585,7 +656,7 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so"
                     + " abort", active);
             onAbort(active);
-            return;
+            return true;
         }
 
         final int changeSize = info.getChanges().size();
@@ -615,16 +686,17 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     "Non-visible anim so abort: %s", active);
             onAbort(active);
-            return;
+            return true;
         }
 
         setupStartState(active.mInfo, active.mStartT, active.mFinishT);
 
-        if (mReadyTransitions.size() > 1) {
+        if (track.mReadyTransitions.size() > 1) {
             // There are already transitions waiting in the queue, so just return.
-            return;
+            return true;
         }
-        processReadyQueue();
+        processReadyQueue(track);
+        return true;
     }
 
     /**
@@ -644,25 +716,53 @@
         return false;
     }
 
-    void processReadyQueue() {
-        if (mReadyTransitions.isEmpty()) {
-            // Check if idle.
-            if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()) {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition "
-                        + "animations finished");
-                // Run all runnables from the run-when-idle queue.
-                for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
-                    mRunWhenIdleQueue.get(i).run();
+    private boolean areTracksIdle() {
+        for (int i = 0; i < mTracks.size(); ++i) {
+            if (!mTracks.get(i).isIdle()) return false;
+        }
+        return true;
+    }
+
+    private boolean isAnimating() {
+        return !mReadyDuringSync.isEmpty() || !areTracksIdle();
+    }
+
+    private boolean isIdle() {
+        return mPendingTransitions.isEmpty() && !isAnimating();
+    }
+
+    void processReadyQueue(Track track) {
+        if (track.mReadyTransitions.isEmpty()) {
+            if (track.mActiveTransition == null) {
+                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Track %d became idle",
+                        mTracks.indexOf(track));
+                if (areTracksIdle()) {
+                    if (!mReadyDuringSync.isEmpty()) {
+                        // Dispatch everything unless we hit another sync
+                        while (!mReadyDuringSync.isEmpty()) {
+                            ActiveTransition next = mReadyDuringSync.remove(0);
+                            boolean success = dispatchReady(next);
+                            // Hit a sync or sleep, so stop dispatching.
+                            if (!success) break;
+                        }
+                    } else if (mPendingTransitions.isEmpty()) {
+                        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition "
+                                + "animations finished");
+                        // Run all runnables from the run-when-idle queue.
+                        for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
+                            mRunWhenIdleQueue.get(i).run();
+                        }
+                        mRunWhenIdleQueue.clear();
+                    }
                 }
-                mRunWhenIdleQueue.clear();
             }
             return;
         }
-        final ActiveTransition ready = mReadyTransitions.get(0);
-        if (mActiveTransitions.isEmpty()) {
-            // The normal case, just play it (currently we only support 1 active transition).
-            mReadyTransitions.remove(0);
-            mActiveTransitions.add(ready);
+        final ActiveTransition ready = track.mReadyTransitions.get(0);
+        if (track.mActiveTransition == null) {
+            // The normal case, just play it.
+            track.mReadyTransitions.remove(0);
+            track.mActiveTransition = ready;
             if (ready.mAborted) {
                 // finish now since there's nothing to animate. Calls back into processReadyQueue
                 onFinish(ready, null, null);
@@ -670,11 +770,11 @@
             }
             playTransition(ready);
             // Attempt to merge any more queued-up transitions.
-            processReadyQueue();
+            processReadyQueue(track);
             return;
         }
         // An existing animation is playing, so see if we can merge.
-        final ActiveTransition playing = mActiveTransitions.get(0);
+        final ActiveTransition playing = track.mActiveTransition;
         if (ready.mAborted) {
             // record as merged since it is no-op. Calls back into processReadyQueue
             onMerged(playing, ready);
@@ -688,18 +788,23 @@
     }
 
     private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) {
+        if (playing.getTrack() != merged.getTrack()) {
+            throw new IllegalStateException("Can't merge across tracks: " + merged + " into "
+                    + playing);
+        }
+        final Track track = mTracks.get(playing.getTrack());
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s",
                 merged, playing);
         int readyIdx = 0;
-        if (mReadyTransitions.isEmpty() || mReadyTransitions.get(0) != merged) {
+        if (track.mReadyTransitions.isEmpty() || track.mReadyTransitions.get(0) != merged) {
             Log.e(TAG, "Merged transition out-of-order? " + merged);
-            readyIdx = mReadyTransitions.indexOf(merged);
+            readyIdx = track.mReadyTransitions.indexOf(merged);
             if (readyIdx < 0) {
                 Log.e(TAG, "Merged a transition that is no-longer queued? " + merged);
                 return;
             }
         }
-        mReadyTransitions.remove(readyIdx);
+        track.mReadyTransitions.remove(readyIdx);
         if (playing.mMerged == null) {
             playing.mMerged = new ArrayList<>();
         }
@@ -712,7 +817,7 @@
             mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken);
         }
         // See if we should merge another transition.
-        processReadyQueue();
+        processReadyQueue(track);
     }
 
     private void playTransition(@NonNull ActiveTransition active) {
@@ -781,6 +886,7 @@
 
     /** Aborts a transition. This will still queue it up to maintain order. */
     private void onAbort(ActiveTransition transition) {
+        final Track track = mTracks.get(transition.getTrack());
         // apply immediately since they may be "parallel" operations: We currently we use abort for
         // thing which are independent to other transitions (like starting-window transfer).
         transition.mStartT.apply();
@@ -796,11 +902,11 @@
         releaseSurfaces(transition.mInfo);
 
         // This still went into the queue (to maintain the correct finish ordering).
-        if (mReadyTransitions.size() > 1) {
+        if (track.mReadyTransitions.size() > 1) {
             // There are already transitions waiting in the queue, so just return.
             return;
         }
-        processReadyQueue();
+        processReadyQueue(track);
     }
 
     /**
@@ -815,17 +921,14 @@
     private void onFinish(ActiveTransition active,
             @Nullable WindowContainerTransaction wct,
             @Nullable WindowContainerTransactionCallback wctCB) {
-        int activeIdx = mActiveTransitions.indexOf(active);
-        if (activeIdx < 0) {
+        final Track track = mTracks.get(active.getTrack());
+        if (track.mActiveTransition != active) {
             Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
                     + " a handler didn't properly deal with a merge. " + active,
                     new RuntimeException());
             return;
-        } else if (activeIdx != 0) {
-            // Relevant right now since we only allow 1 active transition at a time.
-            Log.e(TAG, "Finishing a transition out of order. " + active);
         }
-        mActiveTransitions.remove(activeIdx);
+        track.mActiveTransition = null;
 
         for (int i = 0; i < mObservers.size(); ++i) {
             mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted);
@@ -877,18 +980,20 @@
         }
 
         // Now that this is done, check the ready queue for more work.
-        processReadyQueue();
+        processReadyQueue(track);
     }
 
     private boolean isTransitionKnown(IBinder token) {
         for (int i = 0; i < mPendingTransitions.size(); ++i) {
             if (mPendingTransitions.get(i).mToken == token) return true;
         }
-        for (int i = 0; i < mReadyTransitions.size(); ++i) {
-            if (mReadyTransitions.get(i).mToken == token) return true;
-        }
-        for (int i = 0; i < mActiveTransitions.size(); ++i) {
-            final ActiveTransition active = mActiveTransitions.get(i);
+        for (int t = 0; t < mTracks.size(); ++t) {
+            final Track tr = mTracks.get(t);
+            for (int i = 0; i < tr.mReadyTransitions.size(); ++i) {
+                if (tr.mReadyTransitions.get(i).mToken == token) return true;
+            }
+            final ActiveTransition active = tr.mActiveTransition;
+            if (active == null) continue;
             if (active.mToken == token) return true;
             if (active.mMerged == null) continue;
             for (int m = 0; m < active.mMerged.size(); ++m) {
@@ -963,7 +1068,7 @@
      *
      * This works by "merging" the sleep transition into the currently-playing transition (even if
      * its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish
-     * within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and
+     * within `SYNC_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and
      * send an abort/consumed message).
      *
      * This is then repeated until there are no more pending sleep transitions.
@@ -971,48 +1076,53 @@
      * @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
      *                    signal to -- so it will be force-finished if it's still running.
      */
-    private void finishForSleep(@Nullable ActiveTransition forceFinish) {
-        if ((mActiveTransitions.isEmpty() && mReadyTransitions.isEmpty())
-                || mSleepHandler.mSleepTransitions.isEmpty()) {
-            // Done finishing things.
-            // Prevent any weird leaks... shouldn't happen though.
-            mSleepHandler.mSleepTransitions.clear();
-            return;
-        }
-        if (forceFinish != null && mActiveTransitions.contains(forceFinish)) {
-            Log.e(TAG, "Forcing transition to finish due to sleep timeout: " + forceFinish);
-            forceFinish.mAborted = true;
-            // Last notify of it being consumed. Note: mHandler should never be null,
-            // but check just to be safe.
-            if (forceFinish.mHandler != null) {
-                forceFinish.mHandler.onTransitionConsumed(
-                        forceFinish.mToken, true /* aborted */, null /* finishTransaction */);
+    private void finishForSync(int trackIdx, @Nullable ActiveTransition forceFinish) {
+        final Track track = mTracks.get(trackIdx);
+        if (forceFinish != null) {
+            final Track trk = mTracks.get(forceFinish.getTrack());
+            if (trk != track) {
+                Log.e(TAG, "finishForSleep: mismatched Tracks between forceFinish and logic "
+                        + forceFinish.getTrack() + " vs " + trackIdx);
             }
-            onFinish(forceFinish, null, null);
+            if (trk.mActiveTransition == forceFinish) {
+                Log.e(TAG, "Forcing transition to finish due to sync timeout: " + forceFinish);
+                forceFinish.mAborted = true;
+                // Last notify of it being consumed. Note: mHandler should never be null,
+                // but check just to be safe.
+                if (forceFinish.mHandler != null) {
+                    forceFinish.mHandler.onTransitionConsumed(
+                            forceFinish.mToken, true /* aborted */, null /* finishTransaction */);
+                }
+                onFinish(forceFinish, null, null);
+            }
+        }
+        if (track.isIdle() || mReadyDuringSync.isEmpty()) {
+            // Done finishing things.
+            return;
         }
         final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction();
         final TransitionInfo dummyInfo = new TransitionInfo(TRANSIT_SLEEP, 0 /* flags */);
-        while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) {
-            final ActiveTransition playing = mActiveTransitions.get(0);
-            int sleepIdx = findByToken(mReadyTransitions, mSleepHandler.mSleepTransitions.get(0));
-            if (sleepIdx >= 0) {
-                // Try to signal that we are sleeping by attempting to merge the sleep transition
-                // into the playing one.
-                final ActiveTransition nextSleep = mReadyTransitions.get(sleepIdx);
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge SLEEP %s"
-                        + " into %s", nextSleep, playing);
-                playing.mHandler.mergeAnimation(nextSleep.mToken, dummyInfo, dummyT,
-                        playing.mToken, (wct, cb) -> {});
-            } else {
-                Log.e(TAG, "Couldn't find sleep transition in ready list: "
-                        + mSleepHandler.mSleepTransitions.get(0));
+        while (track.mActiveTransition != null && !mReadyDuringSync.isEmpty()) {
+            final ActiveTransition playing = track.mActiveTransition;
+            final ActiveTransition nextSync = mReadyDuringSync.get(0);
+            if (!nextSync.isSync()) {
+                Log.e(TAG, "Somehow blocked on a non-sync transition? " + nextSync);
             }
+            // Attempt to merge a SLEEP info to signal that the playing transition needs to
+            // fast-forward.
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s"
+                    + " into %s via a SLEEP proxy", nextSync, playing);
+            playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT,
+                    playing.mToken, (wct, cb) -> {});
             // it's possible to complete immediately. If that happens, just repeat the signal
             // loop until we either finish everything or start playing an animation that isn't
             // finishing immediately.
-            if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) {
-                // Give it a (very) short amount of time to process it before forcing.
-                mMainExecutor.executeDelayed(() -> finishForSleep(playing), SLEEP_ALLOWANCE_MS);
+            if (track.mActiveTransition == playing) {
+                if (!mDisableForceSync) {
+                    // Give it a short amount of time to process it before forcing.
+                    mMainExecutor.executeDelayed(() -> finishForSync(trackIdx, playing),
+                            SYNC_ALLOWANCE_MS);
+                }
                 break;
             }
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
index da95c77..4998702 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestShellExecutor.java
@@ -48,9 +48,8 @@
     }
 
     public void flushAll() {
-        for (Runnable r : mRunnables) {
-            r.run();
+        while (!mRunnables.isEmpty()) {
+            mRunnables.remove(0).run();
         }
-        mRunnables.clear();
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 5cd548b..8eb5c6a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -30,10 +30,12 @@
 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_SLEEP;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
+import static android.window.TransitionInfo.FLAG_SYNC;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
@@ -63,6 +65,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
@@ -587,7 +590,8 @@
         requestStartTransition(transitions, transitTokenNotReady);
 
         mDefaultHandler.setSimulateMerge(true);
-        mDefaultHandler.mFinishes.get(0).onTransitionFinished(null /* wct */, null /* wctCB */);
+        mDefaultHandler.mFinishes.get(0).second.onTransitionFinished(
+                null /* wct */, null /* wctCB */);
 
         // Make sure that the non-ready transition is not merged.
         assertEquals(0, mDefaultHandler.mergeCount());
@@ -1059,6 +1063,223 @@
         assertEquals(1, mDefaultHandler.activeCount());
     }
 
+    @Test
+    public void testMultipleTracks() {
+        Transitions transitions = createTestTransitions();
+        transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+        TestTransitionHandler alwaysMergeHandler = new TestTransitionHandler();
+        alwaysMergeHandler.setSimulateMerge(true);
+
+        final boolean[] becameIdle = new boolean[]{false};
+
+        final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+        final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+        // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+        // different track.
+        IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, alwaysMergeHandler);
+        // start tracking idle
+        transitions.runOnIdle(() -> becameIdle[0] = true);
+
+        IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+        IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+
+        TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoA.setTrack(0);
+        TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoB.setTrack(1);
+        TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        infoC.setTrack(1);
+
+        transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+        assertEquals(1, alwaysMergeHandler.activeCount());
+        transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+        // should now be running in parallel
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, alwaysMergeHandler.activeCount());
+        // make sure we didn't try to merge into a different track.
+        assertEquals(0, alwaysMergeHandler.mergeCount());
+
+        // This should be queued-up since it is on track 1 (same as B)
+        transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, alwaysMergeHandler.activeCount());
+
+        // Now finish B and make sure C starts
+        mDefaultHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        // Now C and A running in parallel
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, alwaysMergeHandler.activeCount());
+        assertEquals(0, alwaysMergeHandler.mergeCount());
+
+        // Finish A
+        alwaysMergeHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        // C still running
+        assertEquals(0, alwaysMergeHandler.activeCount());
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertFalse(becameIdle[0]);
+
+        mDefaultHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        assertEquals(0, mDefaultHandler.activeCount());
+        assertTrue(becameIdle[0]);
+    }
+
+    @Test
+    public void testSyncMultipleTracks() {
+        Transitions transitions = createTestTransitions();
+        transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+        TestTransitionHandler secondHandler = new TestTransitionHandler();
+
+        // Disable the forced early-sync-finish so that we can test the ordering mechanics.
+        transitions.setDisableForceSyncForTest(true);
+        mDefaultHandler.mFinishOnSync = false;
+        secondHandler.mFinishOnSync = false;
+
+        final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+        final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+        // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+        // different track.
+        IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+        IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+        IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler);
+        IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+        IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+        IBinder transitE = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+
+        TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoA.setTrack(0);
+        TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoB.setTrack(1);
+        TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        infoC.setTrack(1);
+        TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        infoSync.setTrack(0);
+        infoSync.setFlags(FLAG_SYNC);
+        TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoD.setTrack(1);
+        TransitionInfo infoE = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoE.setTrack(0);
+
+        // Start A B and C where A is track 0, B and C are track 1 (C should be queued)
+        transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+        // should now be running in parallel (with one queued)
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, secondHandler.activeCount());
+
+        // Make the sync ready and the following (D, E) ready.
+        transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitE, infoE, mockSCT, mockSCT);
+
+        // nothing should have happened yet since the sync is queued and blocking everything.
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, secondHandler.activeCount());
+
+        // Finish A (which is track 0 like the sync).
+        mDefaultHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        // Even though the sync is on track 0 and track 0 became idle, it should NOT be started yet
+        // because it must wait for everything. Additionally, D/E shouldn't start yet either.
+        assertEquals(0, mDefaultHandler.activeCount());
+        assertEquals(1, secondHandler.activeCount());
+
+        // Now finish B and C -- this should then allow the sync to start and D to run (in parallel)
+        secondHandler.finishOne();
+        secondHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        // Now the sync and D (on track 1) should be running
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, secondHandler.activeCount());
+
+        // finish the sync. track 0 still has E
+        mDefaultHandler.finishOne();
+        mMainExecutor.flushAll();
+        assertEquals(1, mDefaultHandler.activeCount());
+
+        mDefaultHandler.finishOne();
+        secondHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        assertEquals(0, mDefaultHandler.activeCount());
+        assertEquals(0, secondHandler.activeCount());
+    }
+
+    @Test
+    public void testForceSyncTracks() {
+        Transitions transitions = createTestTransitions();
+        transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+        TestTransitionHandler secondHandler = new TestTransitionHandler();
+
+        final WindowContainerTransaction emptyWCT = new WindowContainerTransaction();
+        final SurfaceControl.Transaction mockSCT = mock(SurfaceControl.Transaction.class);
+
+        // Make this always merge so we can ensure that it does NOT get a merge-attempt for a
+        // different track.
+        IBinder transitA = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+        IBinder transitB = transitions.startTransition(TRANSIT_OPEN, emptyWCT, mDefaultHandler);
+        IBinder transitC = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, secondHandler);
+        IBinder transitD = transitions.startTransition(TRANSIT_OPEN, emptyWCT, secondHandler);
+        IBinder transitSync = transitions.startTransition(TRANSIT_CLOSE, emptyWCT, mDefaultHandler);
+
+        TransitionInfo infoA = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoA.setTrack(0);
+        TransitionInfo infoB = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoB.setTrack(0);
+        TransitionInfo infoC = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        infoC.setTrack(1);
+        TransitionInfo infoD = new TransitionInfoBuilder(TRANSIT_OPEN)
+                .addChange(TRANSIT_OPEN).build();
+        infoD.setTrack(1);
+        TransitionInfo infoSync = new TransitionInfoBuilder(TRANSIT_CLOSE)
+                .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+        infoSync.setTrack(0);
+        infoSync.setFlags(FLAG_SYNC);
+
+        transitions.onTransitionReady(transitA, infoA, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitB, infoB, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitC, infoC, mockSCT, mockSCT);
+        transitions.onTransitionReady(transitD, infoD, mockSCT, mockSCT);
+        // should now be running in parallel (with one queued in each)
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(1, secondHandler.activeCount());
+
+        // Make the sync ready.
+        transitions.onTransitionReady(transitSync, infoSync, mockSCT, mockSCT);
+        mMainExecutor.flushAll();
+
+        // Everything should be forced-finish now except the sync
+        assertEquals(1, mDefaultHandler.activeCount());
+        assertEquals(0, secondHandler.activeCount());
+
+        mDefaultHandler.finishOne();
+        mMainExecutor.flushAll();
+
+        assertEquals(0, mDefaultHandler.activeCount());
+    }
+
     class ChangeBuilder {
         final TransitionInfo.Change mChange;
 
@@ -1097,9 +1318,11 @@
     }
 
     class TestTransitionHandler implements Transitions.TransitionHandler {
-        ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>();
+        ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> mFinishes =
+                new ArrayList<>();
         final ArrayList<IBinder> mMerged = new ArrayList<>();
         boolean mSimulateMerge = false;
+        boolean mFinishOnSync = true;
         final ArraySet<IBinder> mShouldMerge = new ArraySet<>();
 
         @Override
@@ -1107,7 +1330,7 @@
                 @NonNull SurfaceControl.Transaction startTransaction,
                 @NonNull SurfaceControl.Transaction finishTransaction,
                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            mFinishes.add(finishCallback);
+            mFinishes.add(new Pair<>(transition, finishCallback));
             return true;
         }
 
@@ -1115,6 +1338,13 @@
         public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
+            if (mFinishOnSync && info.getType() == TRANSIT_SLEEP) {
+                for (int i = 0; i < mFinishes.size(); ++i) {
+                    if (mFinishes.get(i).first != mergeTarget) continue;
+                    mFinishes.remove(i).second.onTransitionFinished(null, null);
+                    return;
+                }
+            }
             if (!(mSimulateMerge || mShouldMerge.contains(transition))) return;
             mMerged.add(transition);
             finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
@@ -1136,18 +1366,19 @@
         }
 
         void finishAll() {
-            final ArrayList<Transitions.TransitionFinishCallback> finishes = mFinishes;
+            final ArrayList<Pair<IBinder, Transitions.TransitionFinishCallback>> finishes =
+                    mFinishes;
             mFinishes = new ArrayList<>();
             for (int i = finishes.size() - 1; i >= 0; --i) {
-                finishes.get(i).onTransitionFinished(null /* wct */, null /* wctCB */);
+                finishes.get(i).second.onTransitionFinished(null /* wct */, null /* wctCB */);
             }
             mShouldMerge.clear();
         }
 
         void finishOne() {
-            Transitions.TransitionFinishCallback fin = mFinishes.remove(0);
+            Pair<IBinder, Transitions.TransitionFinishCallback> fin = mFinishes.remove(0);
             mMerged.clear();
-            fin.onTransitionFinished(null /* wct */, null /* wctCB */);
+            fin.second.onTransitionFinished(null /* wct */, null /* wctCB */);
         }
 
         int activeCount() {
diff --git a/libs/hwui/DamageAccumulator.cpp b/libs/hwui/DamageAccumulator.cpp
index 9db47c3..a8d170d 100644
--- a/libs/hwui/DamageAccumulator.cpp
+++ b/libs/hwui/DamageAccumulator.cpp
@@ -207,27 +207,6 @@
     }
 }
 
-static void computeTransformImpl(const DirtyStack* frame, const DirtyStack* end,
-                                 Matrix4* outMatrix) {
-  while (frame != end) {
-    switch (frame->type) {
-        case TransformRenderNode:
-            frame->renderNode->applyViewPropertyTransforms(*outMatrix);
-            break;
-        case TransformMatrix4:
-            outMatrix->multiply(*frame->matrix4);
-            break;
-        case TransformNone:
-            // nothing to be done
-            break;
-        default:
-            LOG_ALWAYS_FATAL("Tried to compute transform with an invalid type: %d",
-                             frame->type);
-    }
-    frame = frame->prev;
-  }
-}
-
 void DamageAccumulator::applyRenderNodeTransform(DirtyStack* frame) {
     if (frame->pendingDirty.isEmpty()) {
         return;
@@ -282,9 +261,6 @@
 
 DamageAccumulator::StretchResult DamageAccumulator::findNearestStretchEffect() const {
     DirtyStack* frame = mHead;
-    const auto& headProperties = mHead->renderNode->properties();
-    float startWidth = headProperties.getWidth();
-    float startHeight = headProperties.getHeight();
     while (frame->prev != frame) {
         if (frame->type == TransformRenderNode) {
             const auto& renderNode = frame->renderNode;
@@ -295,21 +271,16 @@
             const float height = (float) frameRenderNodeProperties.getHeight();
             if (!effect.isEmpty()) {
                 Matrix4 stretchMatrix;
-                computeTransformImpl(mHead, frame, &stretchMatrix);
-                Rect stretchRect = Rect(0.f, 0.f, startWidth, startHeight);
+                computeTransformImpl(frame, &stretchMatrix);
+                Rect stretchRect = Rect(0.f, 0.f, width, height);
                 stretchMatrix.mapRect(stretchRect);
 
                 return StretchResult{
-                    .stretchEffect = &effect,
-                    .childRelativeBounds = SkRect::MakeLTRB(
-                        stretchRect.left,
-                        stretchRect.top,
-                        stretchRect.right,
-                        stretchRect.bottom
-                    ),
-                    .width = width,
-                    .height = height
-                };
+                        .stretchEffect = &effect,
+                        .parentBounds = SkRect::MakeLTRB(stretchRect.left, stretchRect.top,
+                                                         stretchRect.right, stretchRect.bottom),
+                        .width = width,
+                        .height = height};
             }
         }
         frame = frame->prev;
diff --git a/libs/hwui/DamageAccumulator.h b/libs/hwui/DamageAccumulator.h
index 90a3517..c4249af 100644
--- a/libs/hwui/DamageAccumulator.h
+++ b/libs/hwui/DamageAccumulator.h
@@ -70,9 +70,9 @@
         const StretchEffect* stretchEffect;
 
         /**
-         * Bounds of the child relative to the stretch container
+         * Bounds of the stretching container
          */
-        const SkRect childRelativeBounds;
+        const SkRect parentBounds;
 
         /**
          * Width of the stretch container
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index ac1f92de..24a785c 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -584,13 +584,15 @@
             uirenderer::Rect bounds(props.getWidth(), props.getHeight());
             bool useStretchShader =
                     Properties::getStretchEffectBehavior() != StretchEffectBehavior::UniformScale;
-            if (useStretchShader && info.stretchEffectCount) {
+            // Compute the transform bounds first before calculating the stretch
+            transform.mapRect(bounds);
+
+            bool hasStretch = useStretchShader && info.stretchEffectCount;
+            if (hasStretch) {
                 handleStretchEffect(info, bounds);
             }
 
-            transform.mapRect(bounds);
-
-            if (CC_LIKELY(transform.isPureTranslate())) {
+            if (CC_LIKELY(transform.isPureTranslate()) && !hasStretch) {
                 // snap/round the computed bounds, so they match the rounding behavior
                 // of the clear done in SurfaceView#draw().
                 bounds.snapGeometryToPixelBoundaries(false);
@@ -665,45 +667,42 @@
             return env;
         }
 
-        void stretchTargetBounds(const StretchEffect& stretchEffect,
-                                 float width, float height,
-                                 const SkRect& childRelativeBounds,
-                                 uirenderer::Rect& bounds) {
-              float normalizedLeft = childRelativeBounds.left() / width;
-              float normalizedTop = childRelativeBounds.top() / height;
-              float normalizedRight = childRelativeBounds.right() / width;
-              float normalizedBottom = childRelativeBounds.bottom() / height;
-              float reverseLeft = width *
-                  (stretchEffect.computeStretchedPositionX(normalizedLeft) -
-                    normalizedLeft);
-              float reverseTop = height *
-                  (stretchEffect.computeStretchedPositionY(normalizedTop) -
-                    normalizedTop);
-              float reverseRight = width *
-                  (stretchEffect.computeStretchedPositionX(normalizedRight) -
-                    normalizedLeft);
-              float reverseBottom = height *
-                  (stretchEffect.computeStretchedPositionY(normalizedBottom) -
-                    normalizedTop);
-              bounds.left = reverseLeft;
-              bounds.top = reverseTop;
-              bounds.right = reverseRight;
-              bounds.bottom = reverseBottom;
-        }
-
         void handleStretchEffect(const TreeInfo& info, uirenderer::Rect& targetBounds) {
             // Search up to find the nearest stretcheffect parent
             const DamageAccumulator::StretchResult result =
                 info.damageAccumulator->findNearestStretchEffect();
             const StretchEffect* effect = result.stretchEffect;
-            if (!effect) {
+            if (effect) {
+                // Compute the number of pixels that the stretching container
+                // scales by.
+                // Then compute the scale factor that the child would need
+                // to scale in order to occupy the same pixel bounds.
+                auto& parentBounds = result.parentBounds;
+                auto parentWidth = parentBounds.width();
+                auto parentHeight = parentBounds.height();
+                auto& stretchDirection = effect->getStretchDirection();
+                auto stretchX = stretchDirection.x();
+                auto stretchY = stretchDirection.y();
+                auto stretchXPixels = parentWidth * std::abs(stretchX);
+                auto stretchYPixels = parentHeight * std::abs(stretchY);
+                SkMatrix stretchMatrix;
+
+                auto childScaleX = 1 + (stretchXPixels / targetBounds.getWidth());
+                auto childScaleY = 1 + (stretchYPixels / targetBounds.getHeight());
+                auto pivotX = stretchX > 0 ? targetBounds.left : targetBounds.right;
+                auto pivotY = stretchY > 0 ? targetBounds.top : targetBounds.bottom;
+                stretchMatrix.setScale(childScaleX, childScaleY, pivotX, pivotY);
+                SkRect rect = SkRect::MakeLTRB(targetBounds.left, targetBounds.top,
+                                               targetBounds.right, targetBounds.bottom);
+                SkRect dst = stretchMatrix.mapRect(rect);
+                targetBounds.left = dst.left();
+                targetBounds.top = dst.top();
+                targetBounds.right = dst.right();
+                targetBounds.bottom = dst.bottom();
+            } else {
                 return;
             }
 
-            const auto& childRelativeBounds = result.childRelativeBounds;
-            stretchTargetBounds(*effect, result.width, result.height,
-                                childRelativeBounds,targetBounds);
-
             if (Properties::getStretchEffectBehavior() ==
                 StretchEffectBehavior::Shader) {
                 JNIEnv* env = jnienv();
@@ -714,9 +713,8 @@
                         gPositionListener.clazz, gPositionListener.callApplyStretch, mListener,
                         info.canvasContext.getFrameNumber(), result.width, result.height,
                         stretchDirection.fX, stretchDirection.fY, effect->maxStretchAmountX,
-                        effect->maxStretchAmountY, childRelativeBounds.left(),
-                        childRelativeBounds.top(), childRelativeBounds.right(),
-                        childRelativeBounds.bottom());
+                        effect->maxStretchAmountY, targetBounds.left, targetBounds.top,
+                        targetBounds.right, targetBounds.bottom);
                 if (!keepListening) {
                     env->DeleteGlobalRef(mListener);
                     mListener = nullptr;
diff --git a/media/java/android/media/ImageUtils.java b/media/java/android/media/ImageUtils.java
index 7ac8446..f4caad7 100644
--- a/media/java/android/media/ImageUtils.java
+++ b/media/java/android/media/ImageUtils.java
@@ -20,6 +20,7 @@
 import android.graphics.PixelFormat;
 import android.hardware.HardwareBuffer;
 import android.media.Image.Plane;
+import android.util.Log;
 import android.util.Size;
 
 import libcore.io.Memory;
@@ -30,6 +31,7 @@
  * Package private utility class for hosting commonly used Image related methods.
  */
 class ImageUtils {
+    private static final String IMAGEUTILS_LOG_TAG = "ImageUtils";
 
     /**
      * Only a subset of the formats defined in
@@ -266,11 +268,15 @@
                 break;
             case PixelFormat.RGBA_8888:
             case PixelFormat.RGBX_8888:
+            case PixelFormat.RGBA_1010102:
                 estimatedBytePerPixel = 4.0;
                 break;
             default:
-                throw new UnsupportedOperationException(
-                        String.format("Invalid format specified %d", format));
+                if (Log.isLoggable(IMAGEUTILS_LOG_TAG, Log.VERBOSE)) {
+                    Log.v(IMAGEUTILS_LOG_TAG, "getEstimatedNativeAllocBytes() uses default"
+                            + "estimated native allocation size.");
+                }
+                estimatedBytePerPixel = 1.0;
         }
 
         return (int)(width * height * estimatedBytePerPixel * numImages);
@@ -295,6 +301,7 @@
                 }
             case PixelFormat.RGB_565:
             case PixelFormat.RGBA_8888:
+            case PixelFormat.RGBA_1010102:
             case PixelFormat.RGBX_8888:
             case PixelFormat.RGB_888:
             case ImageFormat.JPEG:
@@ -312,8 +319,11 @@
             case ImageFormat.PRIVATE:
                 return new Size(0, 0);
             default:
-                throw new UnsupportedOperationException(
-                        String.format("Invalid image format %d", image.getFormat()));
+                if (Log.isLoggable(IMAGEUTILS_LOG_TAG, Log.VERBOSE)) {
+                    Log.v(IMAGEUTILS_LOG_TAG, "getEffectivePlaneSizeForImage() uses"
+                            + "image's width and height for plane size.");
+                }
+                return new Size(image.getWidth(), image.getHeight());
         }
     }
 
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index c41bd1b..b6d70af 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
 import android.annotation.TestApi;
@@ -56,6 +57,7 @@
 import com.android.internal.util.Preconditions;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.Executor;
@@ -74,6 +76,7 @@
     private static final String TAG = "SoundTriggerManager";
 
     private final Context mContext;
+    private final ISoundTriggerService mSoundTriggerService;
     private final ISoundTriggerSession mSoundTriggerSession;
     private final IBinder mBinderToken = new Binder();
 
@@ -114,13 +117,97 @@
                 }
             }
         } catch (RemoteException e) {
-            throw e.rethrowAsRuntimeException();
+            throw e.rethrowFromSystemServer();
         }
         mContext = context;
+        mSoundTriggerService = soundTriggerService;
         mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>();
     }
 
     /**
+     * Construct a {@link SoundTriggerManager} which connects to a specified module.
+     *
+     * @param moduleProperties - Properties representing the module to attach to
+     * @return - A new {@link SoundTriggerManager} which interfaces with the test module.
+     * @hide
+     */
+    @TestApi
+    @SuppressLint("ManagerLookup")
+    public @NonNull SoundTriggerManager createManagerForModule(
+            @NonNull ModuleProperties moduleProperties) {
+        return new SoundTriggerManager(mContext, mSoundTriggerService,
+                Objects.requireNonNull(moduleProperties));
+    }
+
+    /**
+     * Construct a {@link SoundTriggerManager} which connects to a ST module
+     * which is available for instrumentation through {@link attachInstrumentation}.
+     *
+     * @return - A new {@link SoundTriggerManager} which interfaces with the test module.
+     * @hide
+     */
+    @TestApi
+    @SuppressLint("ManagerLookup")
+    public @NonNull SoundTriggerManager createManagerForTestModule() {
+        return new SoundTriggerManager(mContext, mSoundTriggerService, getTestModuleProperties());
+    }
+
+    private final @NonNull SoundTrigger.ModuleProperties getTestModuleProperties() {
+        var moduleProps = listModuleProperties()
+                .stream()
+                .filter((SoundTrigger.ModuleProperties prop)
+                        -> prop.getSupportedModelArch().equals(SoundTrigger.FAKE_HAL_ARCH))
+                .findFirst()
+                .orElse(null);
+        if (moduleProps == null) {
+            throw new IllegalStateException("Fake ST HAL should always be available");
+        }
+        return moduleProps;
+    }
+
+    // Helper constructor to create a manager object attached to a specific ST module.
+    private SoundTriggerManager(@NonNull Context context,
+            @NonNull ISoundTriggerService soundTriggerService,
+            @NonNull ModuleProperties properties) {
+        try {
+            Identity originatorIdentity = new Identity();
+            originatorIdentity.packageName = ActivityThread.currentOpPackageName();
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                mSoundTriggerSession = soundTriggerService.attachAsOriginator(
+                                            originatorIdentity,
+                                            Objects.requireNonNull(properties),
+                                            mBinderToken);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        mContext = Objects.requireNonNull(context);
+        mSoundTriggerService = Objects.requireNonNull(soundTriggerService);
+        mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>();
+    }
+
+    /**
+     * Enumerate the available ST modules. Use {@link createManagerForModule(ModuleProperties)} to
+     * receive a {@link SoundTriggerManager} attached to a specified ST module.
+     * @return - List of available ST modules to attach to.
+     * @hide
+     */
+    @TestApi
+    public static @NonNull List<ModuleProperties> listModuleProperties() {
+        try {
+            ISoundTriggerService service = ISoundTriggerService.Stub.asInterface(
+                    ServiceManager.getService(Context.SOUND_TRIGGER_SERVICE));
+            Identity originatorIdentity = new Identity();
+            originatorIdentity.packageName = ActivityThread.currentOpPackageName();
+            try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+                return service.listModuleProperties(originatorIdentity);
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Updates the given sound trigger model.
      * @deprecated replace with {@link #loadSoundModel}
      * SoundTriggerService model database will be removed
@@ -317,6 +404,17 @@
         SoundTrigger.GenericSoundModel getGenericSoundModel() {
             return mGenericSoundModel;
         }
+
+        /**
+         * Return a {@link SoundTrigger.SoundModel} view of the model for
+         * test purposes.
+         * @hide
+         */
+        @TestApi
+        public @NonNull SoundTrigger.SoundModel getSoundModel() {
+            return mGenericSoundModel;
+        }
+
     }
 
 
@@ -369,7 +467,8 @@
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
     @UnsupportedAppUsage
-    public int loadSoundModel(SoundModel soundModel) {
+    @TestApi
+    public int loadSoundModel(@NonNull SoundModel soundModel) {
         if (soundModel == null || mSoundTriggerSession == null) {
             return STATUS_ERROR;
         }
diff --git a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
index 6c463e1..6bfcd82 100644
--- a/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
+++ b/packages/CompanionDeviceManager/res/layout/list_item_permission.xml
@@ -22,7 +22,8 @@
               android:orientation="horizontal"
               android:paddingStart="32dp"
               android:paddingEnd="32dp"
-              android:paddingTop="12dp">
+              android:paddingTop="6dp"
+              android:paddingBottom="6dp">
 
     <ImageView
         android:id="@+id/permission_icon"
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index ebfb86d..256e0eb 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -113,6 +113,18 @@
     <!-- Back button for the helper consent dialog [CHAR LIMIT=30] -->
     <string name="consent_back">Back</string>
 
+    <!-- Action when permission list view is expanded CHAR LIMIT=30] -->
+    <string name="permission_expanded">Expanded</string>
+
+    <!-- Expand action permission list CHAR LIMIT=30] -->
+    <string name="permission_expand">Expand</string>
+
+    <!-- Action when permission list view is collapsed CHAR LIMIT=30] -->
+    <string name="permission_collapsed">Collapsed</string>
+
+    <!-- Collapse action permission list CHAR LIMIT=30] -->
+    <string name="permission_collapse">Collapse</string>
+
     <!-- ================== System data transfer ==================== -->
     <!-- Title of the permission sync confirmation dialog. [CHAR LIMIT=NONE] -->
     <string name="permission_sync_confirmation_title">Give apps on &lt;strong&gt;<xliff:g id="companion_device_name" example="Galaxy Watch 5">%1$s</xliff:g>&lt;/strong&gt; the same permissions as on &lt;strong&gt;<xliff:g id="primary_device_name" example="Pixel 6">%2$s</xliff:g>&lt;/strong&gt;?</string>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
index d2fd780..b86ef64 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
@@ -27,6 +27,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
@@ -121,6 +122,10 @@
         if (viewHolder.mExpandButton.getTag() == null) {
             viewHolder.mExpandButton.setTag(R.drawable.btn_expand_more);
         }
+
+        setAccessibility(view, viewType,
+                AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_expand);
+
         // Add expand buttons if the permissions are more than PERMISSION_SIZE in this list also
         // make the summary invisible by default.
         if (mPermissions.size() > PERMISSION_SIZE) {
@@ -132,10 +137,18 @@
                     viewHolder.mExpandButton.setImageResource(R.drawable.btn_expand_less);
                     viewHolder.mPermissionSummary.setVisibility(View.VISIBLE);
                     viewHolder.mExpandButton.setTag(R.drawable.btn_expand_less);
+                    view.setContentDescription(mContext.getString(R.string.permission_expanded));
+                    setAccessibility(view, viewType,
+                            AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_collapse);
+                    viewHolder.mPermissionSummary.setFocusable(true);
                 } else {
                     viewHolder.mExpandButton.setImageResource(R.drawable.btn_expand_more);
                     viewHolder.mPermissionSummary.setVisibility(View.GONE);
                     viewHolder.mExpandButton.setTag(R.drawable.btn_expand_more);
+                    view.setContentDescription(mContext.getString(R.string.permission_collapsed));
+                    setAccessibility(view, viewType,
+                            AccessibilityNodeInfo.ACTION_CLICK, R.string.permission_expanded);
+                    viewHolder.mPermissionSummary.setFocusable(false);
                 }
             });
         } else {
@@ -187,6 +200,18 @@
         }
     }
 
+    private void setAccessibility(View view, int viewType, int action, int resourceId) {
+        final String actionString = mContext.getString(resourceId);
+        final String permission = mContext.getString(sTitleMap.get(viewType));
+        view.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(action,
+                        actionString + permission));
+            }
+        });
+    }
+
     void setPermissionType(List<Integer> permissions) {
         mPermissions = permissions;
     }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
index b60aba8..e6710ff 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
@@ -83,7 +83,7 @@
                 }
 
                 UserManager customUserManager = UninstallUninstalling.this
-                        .createContextAsUser(UserHandle.of(user.getIdentifier()), 0)
+                        .createContextAsUser(user, 0)
                         .getSystemService(UserManager.class);
                 if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) {
                     isCloneUser = true;
@@ -117,7 +117,7 @@
                 int flags = allUsers ? PackageManager.DELETE_ALL_USERS : 0;
                 flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
 
-                getPackageManager().getPackageInstaller().uninstall(
+                createContextAsUser(user, 0).getPackageManager().getPackageInstaller().uninstall(
                         new VersionedPackage(mAppInfo.packageName,
                                 PackageManager.VERSION_CODE_HIGHEST),
                         flags, pendingIntent.getIntentSender());
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
old mode 100755
new mode 100644
index 7250bdd..9c67817
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java
@@ -367,10 +367,10 @@
                 int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0;
                 flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
 
-                getPackageManager().getPackageInstaller().uninstall(
-                        new VersionedPackage(mDialogInfo.appInfo.packageName,
-                                PackageManager.VERSION_CODE_HIGHEST),
-                        flags, pendingIntent.getIntentSender());
+                createContextAsUser(mDialogInfo.user, 0).getPackageManager().getPackageInstaller()
+                        .uninstall(new VersionedPackage(mDialogInfo.appInfo.packageName,
+                                PackageManager.VERSION_CODE_HIGHEST), flags,
+                                pendingIntent.getIntentSender());
             } catch (Exception e) {
                 notificationManager.cancel(uninstallId);
 
diff --git a/packages/SettingsLib/res/drawable/dialog_btn_filled.xml b/packages/SettingsLib/res/drawable/dialog_btn_filled.xml
new file mode 100644
index 0000000..14cb1de9
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/dialog_btn_filled.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/dialog_button_vertical_inset"
+       android:insetBottom="@dimen/dialog_button_vertical_inset">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <solid android:color="?androidprv:attr/colorAccentPrimary"/>
+                <padding android:left="@dimen/dialog_button_horizontal_padding"
+                         android:top="@dimen/dialog_button_vertical_padding"
+                         android:right="@dimen/dialog_button_horizontal_padding"
+                         android:bottom="@dimen/dialog_button_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/packages/SettingsLib/res/drawable/dialog_btn_outline.xml b/packages/SettingsLib/res/drawable/dialog_btn_outline.xml
new file mode 100644
index 0000000..1e77759
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/dialog_btn_outline.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+       xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+       android:insetTop="@dimen/dialog_button_vertical_inset"
+       android:insetBottom="@dimen/dialog_button_vertical_inset">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <solid android:color="@android:color/white"/>
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+            </shape>
+        </item>
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="?android:attr/buttonCornerRadius"/>
+                <solid android:color="@android:color/transparent"/>
+                <stroke android:color="?androidprv:attr/colorAccentPrimaryVariant"
+                        android:width="1dp"
+                />
+                <padding android:left="@dimen/dialog_button_horizontal_padding"
+                         android:top="@dimen/dialog_button_vertical_padding"
+                         android:right="@dimen/dialog_button_horizontal_padding"
+                         android:bottom="@dimen/dialog_button_vertical_padding"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/packages/SettingsLib/res/drawable/ic_admin_panel_settings.xml b/packages/SettingsLib/res/drawable/ic_admin_panel_settings.xml
new file mode 100644
index 0000000..3ea8e9e
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_admin_panel_settings.xml
@@ -0,0 +1,25 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24"
+        android:tint="?android:attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M17,17Q17.625,17 18.062,16.562Q18.5,16.125 18.5,15.5Q18.5,14.875 18.062,14.438Q17.625,14 17,14Q16.375,14 15.938,14.438Q15.5,14.875 15.5,15.5Q15.5,16.125 15.938,16.562Q16.375,17 17,17ZM17,20Q17.775,20 18.425,19.637Q19.075,19.275 19.475,18.675Q18.925,18.35 18.3,18.175Q17.675,18 17,18Q16.325,18 15.7,18.175Q15.075,18.35 14.525,18.675Q14.925,19.275 15.575,19.637Q16.225,20 17,20ZM12,22Q8.525,21.125 6.263,18.012Q4,14.9 4,11.1V5L12,2L20,5V10.675Q19.525,10.475 19.025,10.312Q18.525,10.15 18,10.075V6.4L12,4.15L6,6.4V11.1Q6,12.275 6.312,13.45Q6.625,14.625 7.188,15.688Q7.75,16.75 8.55,17.65Q9.35,18.55 10.325,19.15Q10.6,19.95 11.05,20.675Q11.5,21.4 12.075,21.975Q12.05,21.975 12.038,21.988Q12.025,22 12,22ZM17,22Q14.925,22 13.463,20.538Q12,19.075 12,17Q12,14.925 13.463,13.462Q14.925,12 17,12Q19.075,12 20.538,13.462Q22,14.925 22,17Q22,19.075 20.538,20.538Q19.075,22 17,22ZM12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Q12,11.65 12,11.65Z"/>
+</vector>
diff --git a/packages/SettingsLib/res/layout/dialog_with_icon.xml b/packages/SettingsLib/res/layout/dialog_with_icon.xml
new file mode 100644
index 0000000..9081ca5
--- /dev/null
+++ b/packages/SettingsLib/res/layout/dialog_with_icon.xml
@@ -0,0 +1,99 @@
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:gravity="center"
+              android:padding="@dimen/grant_admin_dialog_padding"
+              android:paddingBottom="0dp">
+    <ImageView
+        android:id="@+id/dialog_with_icon_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:contentDescription=""/>
+    <TextView
+        android:id="@+id/dialog_with_icon_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        style="@style/DialogWithIconTitle"
+        android:text="@string/user_grant_admin_title"
+        android:textDirection="locale"/>
+    <TextView
+        android:id="@+id/dialog_with_icon_message"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:gravity="center"
+        style="@style/TextAppearanceSmall"
+        android:text=""
+        android:textDirection="locale"/>
+    <LinearLayout
+        android:id="@+id/custom_layout"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:paddingBottom="0dp">
+    </LinearLayout>
+    <LinearLayout
+        android:id="@+id/button_panel"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:paddingBottom="0dp">
+        <Button
+            android:id="@+id/button_cancel"
+            style="@style/DialogButtonNegative"
+            android:layout_width="wrap_content"
+            android:buttonCornerRadius="28dp"
+            android:layout_height="wrap_content"
+            android:visibility="gone"/>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" >
+        </Space>
+        <Button
+            android:id="@+id/button_back"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@style/DialogButtonNegative"
+            android:buttonCornerRadius="40dp"
+            android:clickable="true"
+            android:focusable="true"
+            android:text="Back"
+            android:visibility="gone"
+        />
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="0.05"
+        >
+        </Space>
+        <Button
+            android:id="@+id/button_ok"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@style/DialogButtonPositive"
+            android:clickable="true"
+            android:focusable="true"
+            android:visibility="gone"
+        />
+    </LinearLayout>
+</LinearLayout>
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index dbfd1c2..e9aded0 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -112,4 +112,13 @@
 
     <!-- Size of grant admin privileges dialog padding -->
     <dimen name="grant_admin_dialog_padding">16dp</dimen>
+
+    <dimen name="dialog_button_horizontal_padding">16dp</dimen>
+    <dimen name="dialog_button_vertical_padding">8dp</dimen>
+    <!-- The button will be 48dp tall, but the background needs to be 36dp tall -->
+    <dimen name="dialog_button_vertical_inset">6dp</dimen>
+    <dimen name="dialog_top_padding">24dp</dimen>
+    <dimen name="dialog_bottom_padding">18dp</dimen>
+    <dimen name="dialog_side_padding">24dp</dimen>
+    <dimen name="dialog_button_bar_top_padding">32dp</dimen>
 </resources>
diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml
index cc60382..2584be7 100644
--- a/packages/SettingsLib/res/values/styles.xml
+++ b/packages/SettingsLib/res/values/styles.xml
@@ -83,4 +83,39 @@
         <item name="android:textDirection">locale</item>
         <item name="android:ellipsize">end</item>
     </style>
+
+    <style name="DialogWithIconTitle" parent="@android:TextAppearance.DeviceDefault.Headline">
+        <item name="android:textSize">@dimen/broadcast_dialog_title_text_size</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textDirection">locale</item>
+        <item name="android:ellipsize">end</item>
+    </style>
+
+    <style name="DialogButtonPositive"  xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+        <item name="android:buttonCornerRadius">0dp</item>
+        <item name="android:background">@drawable/dialog_btn_filled</item>
+        <item name="android:textColor">?androidprv:attr/textColorOnAccent</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:lineHeight">20sp</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:stateListAnimator">@null</item>
+        <item name="android:minWidth">0dp</item>
+    </style>
+
+    <style name="DialogButtonNegative">
+        <item name="android:buttonCornerRadius">28dp</item>
+        <item name="android:background">@drawable/dialog_btn_outline</item>
+        <item name="android:buttonCornerRadius">28dp</item>
+        <item name="android:textColor">?android:attr/textColorPrimary</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:lineHeight">20sp</item>
+        <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+        <item name="android:stateListAnimator">@null</item>
+        <item name="android:minWidth">0dp</item>
+    </style>
+
+    <style name="TextStyleMessage">
+        <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+        <item name="android:textSize">16dp</item>
+    </style>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
new file mode 100644
index 0000000..de48814
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/utils/CustomDialogHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.utils;
+import android.annotation.IntDef;
+import android.annotation.StringRes;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.settingslib.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class is used to create custom dialog with icon, title, message and custom view that are
+ * horizontally centered.
+ */
+public class CustomDialogHelper {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({ICON, TITLE, MESSAGE, LAYOUT, BACK_BUTTON, NEGATIVE_BUTTON, POSITIVE_BUTTON})
+    public @interface LayoutComponent {}
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({BACK_BUTTON, NEGATIVE_BUTTON, POSITIVE_BUTTON})
+    public @interface LayoutButton {}
+
+    public static final int ICON = 0;
+    public static final int TITLE = 1;
+    public static final int MESSAGE = 2;
+    public static final int LAYOUT = 3;
+    public static final int BACK_BUTTON = 4;
+    public static final int NEGATIVE_BUTTON = 5;
+    public static final int POSITIVE_BUTTON = 6;
+    private View mDialogContent;
+    private Dialog mDialog;
+    private Context mContext;
+    private LayoutInflater mLayoutInflater;
+    private ImageView mDialogIcon;
+    private TextView mDialogTitle;
+    private TextView mDialogMessage;
+    private LinearLayout mCustomLayout;
+    private Button mPositiveButton;
+    private Button mNegativeButton;
+    private Button mBackButton;
+
+    public CustomDialogHelper(Context context) {
+        mContext = context;
+        mLayoutInflater = LayoutInflater.from(context);
+        mDialogContent = mLayoutInflater.inflate(R.layout.dialog_with_icon, null);
+        mDialogIcon = mDialogContent.findViewById(R.id.dialog_with_icon_icon);
+        mDialogTitle = mDialogContent.findViewById(R.id.dialog_with_icon_title);
+        mDialogMessage = mDialogContent.findViewById(R.id.dialog_with_icon_message);
+        mCustomLayout = mDialogContent.findViewById(R.id.custom_layout);
+        mPositiveButton = mDialogContent.findViewById(R.id.button_ok);
+        mNegativeButton = mDialogContent.findViewById(R.id.button_cancel);
+        mBackButton = mDialogContent.findViewById(R.id.button_back);
+        createDialog();
+    }
+
+    /**
+     * Creates dialog with content defined in constructor.
+     */
+    private void createDialog() {
+        mDialog = new AlertDialog.Builder(mContext)
+                .setView(mDialogContent)
+                .setCancelable(true)
+                .create();
+        mDialog.getWindow()
+                .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+    }
+
+    /**
+     * Sets title and listener for positive button.
+     */
+    public CustomDialogHelper setPositiveButton(@StringRes int resid,
+            View.OnClickListener onClickListener) {
+        setButton(POSITIVE_BUTTON, resid, onClickListener);
+        return this;
+    }
+
+    /**
+     * Sets positive button text.
+     */
+    public CustomDialogHelper setPositiveButtonText(@StringRes int resid) {
+        mPositiveButton.setText(resid);
+        return this;
+    }
+
+    /**
+     * Sets title and listener for negative button.
+     */
+    public CustomDialogHelper setNegativeButton(@StringRes int resid,
+            View.OnClickListener onClickListener) {
+        setButton(NEGATIVE_BUTTON, resid, onClickListener);
+        return this;
+    }
+
+    /**
+     * Sets negative button text.
+     */
+    public CustomDialogHelper setNegativeButtonText(@StringRes int resid) {
+        mNegativeButton.setText(resid);
+        return this;
+    }
+
+    /**
+     * Sets title and listener for back button.
+     */
+    public CustomDialogHelper setBackButton(@StringRes int resid,
+            View.OnClickListener onClickListener) {
+        setButton(BACK_BUTTON, resid, onClickListener);
+        return this;
+    }
+
+    /**
+     * Sets title for back button.
+     */
+    public CustomDialogHelper setBackButtonText(@StringRes int resid) {
+        mBackButton.setText(resid);
+        return this;
+    }
+
+    private void setButton(@LayoutButton int whichButton, @StringRes int resid,
+            View.OnClickListener listener) {
+        switch (whichButton) {
+            case POSITIVE_BUTTON :
+                mPositiveButton.setText(resid);
+                mPositiveButton.setVisibility(View.VISIBLE);
+                mPositiveButton.setOnClickListener(listener);
+                break;
+            case NEGATIVE_BUTTON:
+                mNegativeButton.setText(resid);
+                mNegativeButton.setVisibility(View.VISIBLE);
+                mNegativeButton.setOnClickListener(listener);
+                break;
+            case BACK_BUTTON:
+                mBackButton.setText(resid);
+                mBackButton.setVisibility(View.VISIBLE);
+                mBackButton.setOnClickListener(listener);
+                break;
+            default:
+                break;
+        }
+    }
+
+
+    /**
+     * Modifies state of button.
+     * //TODO: modify method to allow setting state for any button.
+     */
+    public CustomDialogHelper setButtonEnabled(boolean enabled) {
+        mPositiveButton.setEnabled(enabled);
+        return this;
+    }
+
+    /**
+     * Sets title of the dialog.
+     */
+    public CustomDialogHelper setTitle(@StringRes int resid) {
+        mDialogTitle.setText(resid);
+        return this;
+    }
+
+    /**
+     * Sets message of the dialog.
+     */
+    public CustomDialogHelper setMessage(@StringRes int resid) {
+        mDialogMessage.setText(resid);
+        return this;
+    }
+
+    /**
+     * Sets icon of the dialog.
+     */
+    public CustomDialogHelper setIcon(Drawable icon) {
+        mDialogIcon.setImageDrawable(icon);
+        return this;
+    }
+
+    /**
+     * Removes all views that were previously added to the custom layout part.
+     */
+    public CustomDialogHelper clearCustomLayout() {
+        mCustomLayout.removeAllViews();
+        return this;
+    }
+
+    /**
+     * Hides custom layout.
+     */
+    public void hideCustomLayout() {
+        mCustomLayout.setVisibility(View.GONE);
+    }
+
+    /**
+     * Shows custom layout.
+     */
+    public void showCustomLayout() {
+        mCustomLayout.setVisibility(View.VISIBLE);
+    }
+
+    /**
+     * Adds view to custom layout.
+     */
+    public CustomDialogHelper addCustomView(View view) {
+        mCustomLayout.addView(view);
+        return this;
+    }
+
+    /**
+     * Returns dialog.
+     */
+    public Dialog getDialog() {
+        return mDialog;
+    }
+
+    /**
+     * Sets visibility of layout component.
+     * @param element part of the layout visibility of which is being changed.
+     * @param isVisible true if visibility is set to View.VISIBLE
+     * @return this
+     */
+    public CustomDialogHelper setVisibility(@LayoutComponent int element, boolean isVisible) {
+        int visibility;
+        if (isVisible) {
+            visibility = View.VISIBLE;
+        } else {
+            visibility = View.GONE;
+        }
+        switch (element) {
+            case ICON:
+                mDialogIcon.setVisibility(visibility);
+                break;
+            case TITLE:
+                mDialogTitle.setVisibility(visibility);
+                break;
+            case MESSAGE:
+                mDialogMessage.setVisibility(visibility);
+                break;
+            case BACK_BUTTON:
+                mBackButton.setVisibility(visibility);
+                break;
+            case NEGATIVE_BUTTON:
+                mNegativeButton.setVisibility(visibility);
+                break;
+            case POSITIVE_BUTTON:
+                mPositiveButton.setVisibility(visibility);
+                break;
+            default:
+                break;
+        }
+        return this;
+    }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a00f401..24de487 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -380,6 +380,9 @@
         <service android:name="SystemUIService"
             android:exported="true"
         />
+        <service android:name=".wallet.controller.WalletContextualLocationsService"
+            android:exported="true"
+            />
 
         <!-- Service for dumping extremely verbose content during a bug report -->
         <service android:name=".dump.SystemUIAuxiliaryDumpService"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
index f0a8211..83e44b6 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontInterpolator.kt
@@ -18,8 +18,10 @@
 
 import android.graphics.fonts.Font
 import android.graphics.fonts.FontVariationAxis
+import android.util.LruCache
 import android.util.MathUtils
 import android.util.MathUtils.abs
+import androidx.annotation.VisibleForTesting
 import java.lang.Float.max
 import java.lang.Float.min
 
@@ -34,6 +36,10 @@
 private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
 private const val FONT_ITALIC_DEFAULT_VALUE = 0f
 
+// Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in
+// frame draw time on a Pixel 6.
+@VisibleForTesting const val FONT_CACHE_MAX_ENTRIES = 10
+
 /** Provide interpolation of two fonts by adjusting font variation settings. */
 class FontInterpolator {
 
@@ -81,8 +87,8 @@
     // Font interpolator has two level caches: one for input and one for font with different
     // variation settings. No synchronization is needed since FontInterpolator is not designed to be
     // thread-safe and can be used only on UI thread.
-    private val interpCache = hashMapOf<InterpKey, Font>()
-    private val verFontCache = hashMapOf<VarFontKey, Font>()
+    private val interpCache = LruCache<InterpKey, Font>(FONT_CACHE_MAX_ENTRIES)
+    private val verFontCache = LruCache<VarFontKey, Font>(FONT_CACHE_MAX_ENTRIES)
 
     // Mutable keys for recycling.
     private val tmpInterpKey = InterpKey(null, null, 0f)
@@ -152,7 +158,7 @@
         tmpVarFontKey.set(start, newAxes)
         val axesCachedFont = verFontCache[tmpVarFontKey]
         if (axesCachedFont != null) {
-            interpCache[InterpKey(start, end, progress)] = axesCachedFont
+            interpCache.put(InterpKey(start, end, progress), axesCachedFont)
             return axesCachedFont
         }
 
@@ -160,8 +166,8 @@
         // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
         // not do any IO work.
         val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
-        interpCache[InterpKey(start, end, progress)] = newFont
-        verFontCache[VarFontKey(start, newAxes)] = newFont
+        interpCache.put(InterpKey(start, end, progress), newFont)
+        verFontCache.put(VarFontKey(start, newAxes), newFont)
         return newFont
     }
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
index 9e9929e..3ee97be 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextAnimator.kt
@@ -24,8 +24,10 @@
 import android.graphics.Typeface
 import android.graphics.fonts.Font
 import android.text.Layout
+import android.util.LruCache
 
 private const val DEFAULT_ANIMATION_DURATION: Long = 300
+private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
 
 typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit
 /**
@@ -114,7 +116,7 @@
 
     private val fontVariationUtils = FontVariationUtils()
 
-    private val typefaceCache = HashMap<String, Typeface?>()
+    private val typefaceCache = LruCache<String, Typeface>(TYPEFACE_CACHE_MAX_ENTRIES)
 
     fun updateLayout(layout: Layout) {
         textInterpolator.layout = layout
@@ -218,12 +220,12 @@
         }
 
         if (!fvar.isNullOrBlank()) {
-            textInterpolator.targetPaint.typeface =
-                typefaceCache.getOrElse(fvar) {
-                    textInterpolator.targetPaint.fontVariationSettings = fvar
+            textInterpolator.targetPaint.typeface = typefaceCache.get(fvar) ?: run {
+                textInterpolator.targetPaint.fontVariationSettings = fvar
+                textInterpolator.targetPaint.typeface?.also {
                     typefaceCache.put(fvar, textInterpolator.targetPaint.typeface)
-                    textInterpolator.targetPaint.typeface
                 }
+            }
         }
 
         if (color != null) {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 6ca7f12..3fda83d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -263,6 +263,13 @@
                 view.animateDoze(dozeState.isActive, !hasJumped)
             }
         }
+
+        override fun onPickerCarouselSwiping(swipingFraction: Float, previewRatio: Float) {
+            // TODO(b/278936436): refactor this part when we change recomputePadding
+            // when on the side, swipingFraction = 0, translationY should offset
+            // the top margin change in recomputePadding to make clock be centered
+            view.translationY = 0.5f * view.bottom * (1 - swipingFraction)
+        }
     }
 
     inner class LargeClockAnimations(
diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml
index 64aa629..331307a0 100644
--- a/packages/SystemUI/res/layout/status_bar.xml
+++ b/packages/SystemUI/res/layout/status_bar.xml
@@ -44,6 +44,8 @@
     <LinearLayout android:id="@+id/status_bar_contents"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:clipChildren="false"
+        android:clipToPadding="false"
         android:paddingStart="@dimen/status_bar_padding_start"
         android:paddingEnd="@dimen/status_bar_padding_end"
         android:paddingTop="@dimen/status_bar_padding_top"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index bbac7b0..4e68efe 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -404,7 +404,34 @@
     <string name="biometric_dialog_last_pin_attempt_before_wipe_profile">If you enter an incorrect PIN on the next attempt, your work profile and its data will be deleted.</string>
     <!-- Content of a dialog shown when the user only has one attempt left to provide the correct password before the work profile is removed. [CHAR LIMIT=NONE] -->
     <string name="biometric_dialog_last_password_attempt_before_wipe_profile">If you enter an incorrect password on the next attempt, your work profile and its data will be deleted.</string>
-
+    <!-- Confirmation button label for a dialog shown when the system requires the user to re-enroll their biometrics. [CHAR LIMIT=20] -->
+    <string name="biometric_re_enroll_dialog_confirm">Set up</string>
+    <!-- Cancel button label for a dialog shown when the system requires the user to re-enroll their biometric. [CHAR LIMIT=20] -->
+    <string name="biometric_re_enroll_dialog_cancel">Not now</string>
+    <!-- Notification content shown when the system requires the user to re-enroll their biometrics. [CHAR LIMIT=NONE] -->
+    <string name="biometric_re_enroll_notification_content">This is required to improve security and performance</string>
+    <!-- Notification title shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_re_enroll_notification_title">Set up Fingerprint Unlock again</string>
+    <!-- Name shown for system notifications related to the fingerprint unlock feature. [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_re_enroll_notification_name">Fingerprint Unlock</string>
+    <!-- Title for a dialog shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_re_enroll_dialog_title">Set up Fingerprint Unlock</string>
+    <!-- Content for a dialog shown when the system requires the user to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_re_enroll_dialog_content">To set up Fingerprint Unlock again, your current fingerprint images and models will be deleted.\n\nAfter they\’re deleted, you\’ll need to set up Fingerprint Unlock again to use your fingerprint to unlock your phone or verify it\’s you.</string>
+    <!-- Content for a dialog shown when the system requires the user to re-enroll their fingerprint (singular). [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_re_enroll_dialog_content_singular">To set up Fingerprint Unlock again, your current fingerprint images and model will be deleted.\n\nAfter they\’re deleted, you\’ll need to set up Fingerprint Unlock again to use your fingerprint to unlock your phone or verify it\’s you.</string>
+    <!-- Content for a dialog shown when an error occurs while the user is trying to re-enroll their fingerprint. [CHAR LIMIT=NONE] -->
+    <string name="fingerprint_reenroll_failure_dialog_content">Couldn\u2019t set up fingerprint unlock. Go to Settings to try again.</string>
+    <!-- Notification title shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
+    <string name="face_re_enroll_notification_title">Set up Face Unlock again</string>
+    <!-- Name shown for system notifications related to the face unlock feature. [CHAR LIMIT=NONE] -->
+    <string name="face_re_enroll_notification_name">Face Unlock</string>
+    <!-- Title for a dialog shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
+    <string name="face_re_enroll_dialog_title">Set up Face Unlock</string>
+    <!-- Content for a dialog shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
+    <string name="face_re_enroll_dialog_content">To set up Face Unlock again, your current face model will be deleted.\n\nYou\’ll need to set up this feature again to use your face to unlock your phone.</string>
+    <!-- Content for a dialog shown when an error occurs while the user is trying to re-enroll their face. [CHAR LIMIT=NONE] -->
+    <string name="face_reenroll_failure_dialog_content">Couldn\u2019t set up face unlock. Go to Settings to try again.</string>
     <!-- Message shown when the system-provided fingerprint dialog is shown, asking for authentication -->
     <string name="fingerprint_dialog_touch_sensor">Touch the fingerprint sensor</string>
     <!-- Message shown to inform the user a face cannot be recognized and fingerprint should instead be used.[CHAR LIMIT=50] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 10c08bc..9573913 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1832,7 +1832,7 @@
             } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
                     .equals(action)) {
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_DPM_STATE_CHANGED,
-                        getSendingUserId()));
+                        getSendingUserId(), 0));
             } else if (ACTION_USER_UNLOCKED.equals(action)) {
                 mHandler.sendMessage(mHandler.obtainMessage(MSG_USER_UNLOCKED,
                         getSendingUserId(), 0));
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java
new file mode 100644
index 0000000..c22a66b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationBroadcastReceiver.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricSourceType;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import javax.inject.Inject;
+
+/**
+ * Receives broadcasts sent by {@link BiometricNotificationService} and takes
+ * the appropriate action.
+ */
+@SysUISingleton
+public class BiometricNotificationBroadcastReceiver extends BroadcastReceiver {
+    static final String ACTION_SHOW_FACE_REENROLL_DIALOG = "face_action_show_reenroll_dialog";
+    static final String ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG =
+            "fingerprint_action_show_reenroll_dialog";
+
+    private static final String TAG = "BiometricNotificationBroadcastReceiver";
+
+    private final Context mContext;
+    private final BiometricNotificationDialogFactory mNotificationDialogFactory;
+    @Inject
+    BiometricNotificationBroadcastReceiver(Context context,
+            BiometricNotificationDialogFactory notificationDialogFactory) {
+        mContext = context;
+        mNotificationDialogFactory = notificationDialogFactory;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+
+        switch (action) {
+            case ACTION_SHOW_FACE_REENROLL_DIALOG:
+                mNotificationDialogFactory.createReenrollDialog(mContext,
+                        new SystemUIDialog(mContext),
+                        BiometricSourceType.FACE)
+                        .show();
+                break;
+            case ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG:
+                mNotificationDialogFactory.createReenrollDialog(
+                        mContext,
+                        new SystemUIDialog(mContext),
+                        BiometricSourceType.FINGERPRINT)
+                        .show();
+                break;
+            default:
+                break;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java
new file mode 100644
index 0000000..3e6508c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationDialogFactory.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.face.Face;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintManager;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import javax.inject.Inject;
+
+/**
+ * Manages the creation of dialogs to be shown for biometric re enroll notifications.
+ */
+@SysUISingleton
+public class BiometricNotificationDialogFactory {
+    private static final String TAG = "BiometricNotificationDialogFactory";
+
+    @Inject
+    BiometricNotificationDialogFactory() {}
+
+    Dialog createReenrollDialog(final Context context, final SystemUIDialog sysuiDialog,
+            BiometricSourceType biometricSourceType) {
+        if (biometricSourceType == BiometricSourceType.FACE) {
+            sysuiDialog.setTitle(context.getString(R.string.face_re_enroll_dialog_title));
+            sysuiDialog.setMessage(context.getString(R.string.face_re_enroll_dialog_content));
+        } else if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
+            FingerprintManager fingerprintManager = context.getSystemService(
+                    FingerprintManager.class);
+            sysuiDialog.setTitle(context.getString(R.string.fingerprint_re_enroll_dialog_title));
+            if (fingerprintManager.getEnrolledFingerprints().size() == 1) {
+                sysuiDialog.setMessage(context.getString(
+                        R.string.fingerprint_re_enroll_dialog_content_singular));
+            } else {
+                sysuiDialog.setMessage(context.getString(
+                        R.string.fingerprint_re_enroll_dialog_content));
+            }
+        }
+
+        sysuiDialog.setPositiveButton(R.string.biometric_re_enroll_dialog_confirm,
+                (dialog, which) -> onReenrollDialogConfirm(context, biometricSourceType));
+        sysuiDialog.setNegativeButton(R.string.biometric_re_enroll_dialog_cancel,
+                (dialog, which) -> {});
+        return sysuiDialog;
+    }
+
+    private static Dialog createReenrollFailureDialog(Context context,
+            BiometricSourceType biometricType) {
+        final SystemUIDialog sysuiDialog = new SystemUIDialog(context);
+
+        if (biometricType == BiometricSourceType.FACE) {
+            sysuiDialog.setMessage(context.getString(
+                    R.string.face_reenroll_failure_dialog_content));
+        } else if (biometricType == BiometricSourceType.FINGERPRINT) {
+            sysuiDialog.setMessage(context.getString(
+                    R.string.fingerprint_reenroll_failure_dialog_content));
+        }
+
+        sysuiDialog.setPositiveButton(R.string.ok, (dialog, which) -> {});
+        return sysuiDialog;
+    }
+
+    private static void onReenrollDialogConfirm(final Context context,
+            BiometricSourceType biometricType) {
+        if (biometricType == BiometricSourceType.FACE) {
+            reenrollFace(context);
+        } else if (biometricType == BiometricSourceType.FINGERPRINT) {
+            reenrollFingerprint(context);
+        }
+    }
+
+    private static void reenrollFingerprint(Context context) {
+        FingerprintManager fingerprintManager = context.getSystemService(FingerprintManager.class);
+        if (fingerprintManager == null) {
+            Log.e(TAG, "Not launching enrollment. Fingerprint manager was null!");
+            createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT).show();
+            return;
+        }
+
+        if (!fingerprintManager.hasEnrolledTemplates(context.getUserId())) {
+            createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT).show();
+            return;
+        }
+
+        // Remove all enrolled fingerprint. Launch enrollment if successful.
+        fingerprintManager.removeAll(context.getUserId(),
+                new FingerprintManager.RemovalCallback() {
+                    boolean mDidShowFailureDialog;
+
+                    @Override
+                    public void onRemovalError(Fingerprint fingerprint, int errMsgId,
+                            CharSequence errString) {
+                        Log.e(TAG, "Not launching enrollment."
+                                + "Failed to remove existing face(s).");
+                        if (!mDidShowFailureDialog) {
+                            mDidShowFailureDialog = true;
+                            createReenrollFailureDialog(context, BiometricSourceType.FINGERPRINT)
+                                    .show();
+                        }
+                    }
+
+                    @Override
+                    public void onRemovalSucceeded(Fingerprint fingerprint, int remaining) {
+                        if (!mDidShowFailureDialog && remaining == 0) {
+                            Intent intent = new Intent(Settings.ACTION_FINGERPRINT_ENROLL);
+                            intent.setPackage("com.android.settings");
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                            context.startActivity(intent);
+                        }
+                    }
+                });
+    }
+
+    private static void reenrollFace(Context context) {
+        FaceManager faceManager = context.getSystemService(FaceManager.class);
+        if (faceManager == null) {
+            Log.e(TAG, "Not launching enrollment. Face manager was null!");
+            createReenrollFailureDialog(context, BiometricSourceType.FACE).show();
+            return;
+        }
+
+        if (!faceManager.hasEnrolledTemplates(context.getUserId())) {
+            createReenrollFailureDialog(context, BiometricSourceType.FACE).show();
+            return;
+        }
+
+        // Remove all enrolled faces. Launch enrollment if successful.
+        faceManager.removeAll(context.getUserId(),
+                new FaceManager.RemovalCallback() {
+                    boolean mDidShowFailureDialog;
+
+                    @Override
+                    public void onRemovalError(Face face, int errMsgId, CharSequence errString) {
+                        Log.e(TAG, "Not launching enrollment."
+                                + "Failed to remove existing face(s).");
+                        if (!mDidShowFailureDialog) {
+                            mDidShowFailureDialog = true;
+                            createReenrollFailureDialog(context, BiometricSourceType.FACE).show();
+                        }
+                    }
+
+                    @Override
+                    public void onRemovalSucceeded(Face face, int remaining) {
+                        if (!mDidShowFailureDialog && remaining == 0) {
+                            Intent intent = new Intent("android.settings.FACE_ENROLL");
+                            intent.setPackage("com.android.settings");
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                            context.startActivity(intent);
+                        }
+                    }
+                });
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java
new file mode 100644
index 0000000..4b17be3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static android.app.PendingIntent.FLAG_IMMUTABLE;
+
+import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG;
+import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricSourceType;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.CoreStartable;
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import javax.inject.Inject;
+
+/**
+ * Handles showing system notifications related to biometric unlock.
+ */
+@SysUISingleton
+public class BiometricNotificationService implements CoreStartable {
+
+    private static final String TAG = "BiometricNotificationService";
+    private static final String CHANNEL_ID = "BiometricHiPriNotificationChannel";
+    private static final String CHANNEL_NAME = " Biometric Unlock";
+    private static final int FACE_NOTIFICATION_ID = 1;
+    private static final int FINGERPRINT_NOTIFICATION_ID = 2;
+    private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds
+    private static final int REENROLL_REQUIRED = 1;
+    private static final int REENROLL_NOT_REQUIRED = 0;
+
+    private final Context mContext;
+    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    private final KeyguardStateController mKeyguardStateController;
+    private final Handler mHandler;
+    private final NotificationManager mNotificationManager;
+    private final BiometricNotificationBroadcastReceiver mBroadcastReceiver;
+    private NotificationChannel mNotificationChannel;
+    private boolean mFaceNotificationQueued;
+    private boolean mFingerprintNotificationQueued;
+    private boolean mFingerprintReenrollRequired;
+
+    private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
+            new KeyguardStateController.Callback() {
+                private boolean mIsShowing = true;
+                @Override
+                public void onKeyguardShowingChanged() {
+                    if (mKeyguardStateController.isShowing()
+                            || mKeyguardStateController.isShowing() == mIsShowing) {
+                        mIsShowing = mKeyguardStateController.isShowing();
+                        return;
+                    }
+                    mIsShowing = mKeyguardStateController.isShowing();
+                    if (isFaceReenrollRequired(mContext) && !mFaceNotificationQueued) {
+                        queueFaceReenrollNotification();
+                    }
+                    if (mFingerprintReenrollRequired && !mFingerprintNotificationQueued) {
+                        mFingerprintReenrollRequired = false;
+                        queueFingerprintReenrollNotification();
+                    }
+                }
+            };
+
+    private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+            new KeyguardUpdateMonitorCallback() {
+                @Override
+                public void onBiometricError(int msgId, String errString,
+                        BiometricSourceType biometricSourceType) {
+                    if (msgId == BiometricFaceConstants.BIOMETRIC_ERROR_RE_ENROLL
+                            && biometricSourceType == BiometricSourceType.FACE) {
+                        Settings.Secure.putIntForUser(mContext.getContentResolver(),
+                                Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_REQUIRED,
+                                UserHandle.USER_CURRENT);
+                    } else if (msgId == BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL
+                            && biometricSourceType == BiometricSourceType.FINGERPRINT) {
+                        mFingerprintReenrollRequired = true;
+                    }
+                }
+            };
+
+
+    @Inject
+    public BiometricNotificationService(Context context,
+            KeyguardUpdateMonitor keyguardUpdateMonitor,
+            KeyguardStateController keyguardStateController,
+            Handler handler, NotificationManager notificationManager,
+            BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver) {
+        mContext = context;
+        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mKeyguardStateController = keyguardStateController;
+        mHandler = handler;
+        mNotificationManager = notificationManager;
+        mBroadcastReceiver = biometricNotificationBroadcastReceiver;
+    }
+
+    @Override
+    public void start() {
+        mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
+        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
+        mNotificationChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
+                NotificationManager.IMPORTANCE_HIGH);
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG);
+        intentFilter.addAction(ACTION_SHOW_FACE_REENROLL_DIALOG);
+        mContext.registerReceiver(mBroadcastReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
+    }
+
+    private void queueFaceReenrollNotification() {
+        mFaceNotificationQueued = true;
+        final String title = mContext.getString(R.string.face_re_enroll_notification_title);
+        final String content = mContext.getString(
+                R.string.biometric_re_enroll_notification_content);
+        final String name = mContext.getString(R.string.face_re_enroll_notification_name);
+        mHandler.postDelayed(
+                () -> showNotification(ACTION_SHOW_FACE_REENROLL_DIALOG, title, content, name,
+                        FACE_NOTIFICATION_ID),
+                SHOW_NOTIFICATION_DELAY_MS);
+    }
+
+    private void queueFingerprintReenrollNotification() {
+        mFingerprintNotificationQueued = true;
+        final String title = mContext.getString(R.string.fingerprint_re_enroll_notification_title);
+        final String content = mContext.getString(
+                R.string.biometric_re_enroll_notification_content);
+        final String name = mContext.getString(R.string.fingerprint_re_enroll_notification_name);
+        mHandler.postDelayed(
+                () -> showNotification(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG, title, content,
+                        name, FINGERPRINT_NOTIFICATION_ID),
+                SHOW_NOTIFICATION_DELAY_MS);
+    }
+
+    private void showNotification(String action, CharSequence title, CharSequence content,
+            CharSequence name, int notificationId) {
+        if (notificationId == FACE_NOTIFICATION_ID) {
+            mFaceNotificationQueued = false;
+        } else if (notificationId == FINGERPRINT_NOTIFICATION_ID) {
+            mFingerprintNotificationQueued = false;
+        }
+
+        if (mNotificationManager == null) {
+            Log.e(TAG, "Failed to show notification "
+                    + action + ". Notification manager is null!");
+            return;
+        }
+
+        final Intent onClickIntent = new Intent(action);
+        final PendingIntent onClickPendingIntent = PendingIntent.getBroadcastAsUser(mContext,
+                0 /* requestCode */, onClickIntent, FLAG_IMMUTABLE, UserHandle.CURRENT);
+
+        final Notification notification = new Notification.Builder(mContext, CHANNEL_ID)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .setSmallIcon(com.android.internal.R.drawable.ic_lock)
+                .setContentTitle(title)
+                .setContentText(content)
+                .setSubText(name)
+                .setContentIntent(onClickPendingIntent)
+                .setAutoCancel(true)
+                .setLocalOnly(true)
+                .setOnlyAlertOnce(true)
+                .setVisibility(Notification.VISIBILITY_SECRET)
+                .build();
+
+        mNotificationManager.createNotificationChannel(mNotificationChannel);
+        mNotificationManager.notifyAsUser(TAG, notificationId, notification, UserHandle.CURRENT);
+    }
+
+    private boolean isFaceReenrollRequired(Context context) {
+        final int settingValue =
+                Settings.Secure.getIntForUser(context.getContentResolver(),
+                        Settings.Secure.FACE_UNLOCK_RE_ENROLL, REENROLL_NOT_REQUIRED,
+                        UserHandle.USER_CURRENT);
+        return settingValue == REENROLL_REQUIRED;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index de84cc2..5d6479e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.accessibility.SystemActions
 import com.android.systemui.accessibility.WindowMagnification
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.biometrics.BiometricNotificationService
 import com.android.systemui.clipboardoverlay.ClipboardListener
 import com.android.systemui.controls.dagger.StartControlsStartableModule
 import com.android.systemui.dagger.qualifiers.PerUser
@@ -75,6 +76,14 @@
     @ClassKey(AuthController::class)
     abstract fun bindAuthController(service: AuthController): CoreStartable
 
+    /** Inject into BiometricNotificationService */
+    @Binds
+    @IntoMap
+    @ClassKey(BiometricNotificationService::class)
+    abstract fun bindBiometricNotificationService(
+        service: BiometricNotificationService
+    ): CoreStartable
+
     /** Inject into ChooserCoreStartable. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index cee8294..dff2c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -288,7 +288,7 @@
             INotificationManager notificationManager,
             IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
-            NotificationInterruptStateProvider interruptionStateProvider,
+            VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
             CommonNotifCollection notifCollection,
@@ -306,7 +306,7 @@
                 notificationManager,
                 dreamManager,
                 visibilityProvider,
-                interruptionStateProvider,
+                visualInterruptionDecisionProvider,
                 zenModeController,
                 notifUserManager,
                 notifCollection,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index aaf4358..05153b6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -87,7 +87,9 @@
         releasedFlag(270682168, "animated_notification_shade_insets")
 
     // TODO(b/268005230): Tracking Bug
-    @JvmField val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim")
+    @JvmField
+    val SENSITIVE_REVEAL_ANIM =
+        unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true)
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
index b86083a..1f13291 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt
@@ -106,7 +106,7 @@
     }
 
     private fun isPhysicalFullKeyboard(deviceId: Int): Boolean {
-        val device = inputManager.getInputDevice(deviceId)
+        val device = inputManager.getInputDevice(deviceId) ?: return false
         return !device.isVirtual && device.isFullKeyboard
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 9ab2e99..2d1b7ae 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -791,7 +791,7 @@
 
             // Translate up from the bottom.
             surfaceBehindMatrix.setTranslate(
-                    surfaceBehindRemoteAnimationTarget.screenSpaceBounds.left.toFloat(),
+                    surfaceBehindRemoteAnimationTarget.localBounds.left.toFloat(),
                     surfaceHeight * SURFACE_BEHIND_START_TRANSLATION_Y * (1f - amount)
             )
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 3770b28..96e9756 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -60,6 +60,7 @@
 private const val MIN_DURATION_COMMITTED_ANIMATION = 80L
 private const val MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION = 120L
 private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
+private const val MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION = 80L
 private const val MIN_DURATION_FLING_ANIMATION = 160L
 
 private const val MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING = 100L
@@ -135,7 +136,8 @@
     private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
     private var previousXTranslationOnActiveOffset = 0f
     private var previousXTranslation = 0f
-    private var totalTouchDelta = 0f
+    private var totalTouchDeltaActive = 0f
+    private var totalTouchDeltaInactive = 0f
     private var touchDeltaStartX = 0f
     private var velocityTracker: VelocityTracker? = null
         set(value) {
@@ -154,7 +156,7 @@
 
     private var gestureEntryTime = 0L
     private var gestureInactiveTime = 0L
-    private var gestureActiveTime = 0L
+    private var gesturePastActiveThresholdWhileInactiveTime = 0L
 
     private val elapsedTimeSinceInactive
         get() = SystemClock.uptimeMillis() - gestureInactiveTime
@@ -250,7 +252,7 @@
     private fun updateConfiguration() {
         params.update(resources)
         mView.updateArrowPaint(params.arrowThickness)
-        minFlingDistance = ViewConfiguration.get(context).scaledTouchSlop * 3
+        minFlingDistance = viewConfiguration.scaledTouchSlop * 3
     }
 
     private val configurationListener = object : ConfigurationController.ConfigurationListener {
@@ -403,30 +405,46 @@
             }
             GestureState.ACTIVE -> {
                 val isPastDynamicDeactivationThreshold =
-                        totalTouchDelta <= params.deactivationSwipeTriggerThreshold
+                    totalTouchDeltaActive <= params.deactivationTriggerThreshold
                 val isMinDurationElapsed =
-                        elapsedTimeSinceEntry > MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION
+                    elapsedTimeSinceEntry > MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION
 
                 if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
-                                isPastDynamicDeactivationThreshold)
+                            isPastDynamicDeactivationThreshold)
                 ) {
                     updateArrowState(GestureState.INACTIVE)
                 }
             }
             GestureState.INACTIVE -> {
                 val isPastStaticThreshold =
-                        xTranslation > params.staticTriggerThreshold
-                val isPastDynamicReactivationThreshold = totalTouchDelta > 0 &&
-                        abs(totalTouchDelta) >=
-                        params.reactivationTriggerThreshold
-
-                if (isPastStaticThreshold &&
+                    xTranslation > params.staticTriggerThreshold
+                val isPastDynamicReactivationThreshold =
+                    totalTouchDeltaInactive >= params.reactivationTriggerThreshold
+                val isPastAllThresholds = isPastStaticThreshold &&
                         isPastDynamicReactivationThreshold &&
                         isWithinYActivationThreshold
-                ) {
+                val isPastAllThresholdsForFirstTime = isPastAllThresholds &&
+                        gesturePastActiveThresholdWhileInactiveTime == 0L
+
+                gesturePastActiveThresholdWhileInactiveTime = when {
+                    isPastAllThresholdsForFirstTime -> SystemClock.uptimeMillis()
+                    isPastAllThresholds -> gesturePastActiveThresholdWhileInactiveTime
+                    else -> 0L
+                }
+
+                val elapsedTimePastAllThresholds =
+                    SystemClock.uptimeMillis() - gesturePastActiveThresholdWhileInactiveTime
+
+                val isPastMinimumInactiveToActiveDuration =
+                    elapsedTimePastAllThresholds > MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
+
+                if (isPastAllThresholds && isPastMinimumInactiveToActiveDuration) {
+                    // The minimum duration adds the 'edge stickiness'
+                    // factor before pulling it off edge
                     updateArrowState(GestureState.ACTIVE)
                 }
             }
+
             else -> {}
         }
     }
@@ -451,19 +469,25 @@
         previousXTranslation = xTranslation
 
         if (abs(xDelta) > 0) {
-            val range =
-                params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold }
-            val isTouchInContinuousDirection =
-                    sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range
+            val isInSameDirection = sign(xDelta) == sign(totalTouchDeltaActive)
+            val isInDynamicRange = totalTouchDeltaActive in params.dynamicTriggerThresholdRange
+            val isTouchInContinuousDirection = isInSameDirection || isInDynamicRange
 
             if (isTouchInContinuousDirection) {
                 // Direction has NOT changed, so keep counting the delta
-                totalTouchDelta += xDelta
+                totalTouchDeltaActive += xDelta
             } else {
                 // Direction has changed, so reset the delta
-                totalTouchDelta = xDelta
+                totalTouchDeltaActive = xDelta
                 touchDeltaStartX = x
             }
+
+            // Add a slop to to prevent small jitters when arrow is at edge in
+            // emitting small values that cause the arrow to poke out slightly
+            val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
+            totalTouchDeltaInactive = totalTouchDeltaInactive
+                .plus(xDelta)
+                .coerceAtLeast(minimumDelta)
         }
 
         updateArrowStateOnMove(yTranslation, xTranslation)
@@ -471,7 +495,7 @@
         val gestureProgress = when (currentState) {
             GestureState.ACTIVE -> fullScreenProgress(xTranslation)
             GestureState.ENTRY -> staticThresholdProgress(xTranslation)
-            GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDelta)
+            GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
             else -> null
         }
 
@@ -529,8 +553,7 @@
      * the arrow is fully stretched (between 0.0 - 1.0f)
      */
     private fun fullScreenProgress(xTranslation: Float): Float {
-        val progress = abs((xTranslation - previousXTranslationOnActiveOffset) /
-                (fullyStretchedThreshold - previousXTranslationOnActiveOffset))
+        val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
         return MathUtils.saturate(progress)
     }
 
@@ -581,13 +604,15 @@
     }
 
     private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator
-    fun preThresholdWidthStretchAmount(progress: Float): Float {
+    private fun preThresholdWidthStretchAmount(progress: Float): Float {
         val interpolator = run {
-            val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop
+            val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
             if (isPastSlop) {
-                if (totalTouchDelta > 0) {
+                if (totalTouchDeltaInactive > 0) {
                     params.entryWidthInterpolator
-                } else params.entryWidthTowardsEdgeInterpolator
+                } else {
+                    params.entryWidthTowardsEdgeInterpolator
+                }
             } else {
                 previousPreThresholdWidthInterpolator
             }.also { previousPreThresholdWidthInterpolator = it }
@@ -643,7 +668,7 @@
             xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
         } ?: 0f
         val isPastFlingVelocityThreshold =
-                flingVelocity > ViewConfiguration.get(context).scaledMinimumFlingVelocity
+                flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
         return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
     }
 
@@ -861,7 +886,6 @@
             }
             GestureState.ACTIVE -> {
                 previousXTranslationOnActiveOffset = previousXTranslation
-                gestureActiveTime = SystemClock.uptimeMillis()
 
                 updateRestingArrowDimens()
 
@@ -870,21 +894,24 @@
                     vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
                 }
 
-                val startingVelocity = convertVelocityToSpringStartingVelocity(
-                    valueOnFastVelocity = 0f,
-                    valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f
-                )
+                val minimumPop = 2f
+                val maximumPop = 4.5f
 
                 when (previousState) {
-                    GestureState.ENTRY,
-                    GestureState.INACTIVE -> {
+                    GestureState.ENTRY -> {
+                        val startingVelocity = convertVelocityToSpringStartingVelocity(
+                            valueOnFastVelocity = minimumPop,
+                            valueOnSlowVelocity = maximumPop,
+                            fastVelocityBound = 1f,
+                            slowVelocityBound = 0.5f,
+                        )
                         mView.popOffEdge(startingVelocity)
                     }
-                    GestureState.COMMITTED -> {
-                        // if previous state was committed then this activation
-                        // was due to a quick second swipe. Don't pop the arrow this time
+                    GestureState.INACTIVE -> {
+                        mView.popOffEdge(maximumPop)
                     }
-                    else -> { }
+
+                    else -> {}
                 }
             }
 
@@ -896,7 +923,7 @@
                 // but because we can also independently enter this state
                 // if touch Y >> touch X, we force it to deactivationSwipeTriggerThreshold
                 // so that gesture progress in this state is consistent regardless of entry
-                totalTouchDelta = params.deactivationSwipeTriggerThreshold
+                totalTouchDeltaInactive = params.deactivationTriggerThreshold
 
                 val startingVelocity = convertVelocityToSpringStartingVelocity(
                         valueOnFastVelocity = -1.05f,
@@ -944,10 +971,12 @@
     private fun convertVelocityToSpringStartingVelocity(
             valueOnFastVelocity: Float,
             valueOnSlowVelocity: Float,
+            fastVelocityBound: Float = 3f,
+            slowVelocityBound: Float = 0f,
     ): Float {
         val factor = velocityTracker?.run {
             computeCurrentVelocity(PX_PER_MS)
-            MathUtils.smoothStep(0f, 3f, abs(xVelocity))
+            MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
         } ?: valueOnFastVelocity
 
         return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
@@ -982,7 +1011,7 @@
                     "$currentState",
                     "startX=$startX",
                     "startY=$startY",
-                    "xDelta=${"%.1f".format(totalTouchDelta)}",
+                    "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
                     "xTranslation=${"%.1f".format(previousXTranslation)}",
                     "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
                     "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
@@ -1023,7 +1052,7 @@
             }
 
             drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
-            drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE)
+            drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
             drawVerticalLine(x = startX, color = Color.GREEN)
             drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
         }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index c9d8c84..876c74a 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -72,9 +72,11 @@
         private set
     var reactivationTriggerThreshold: Float = 0f
         private set
-    var deactivationSwipeTriggerThreshold: Float = 0f
+    var deactivationTriggerThreshold: Float = 0f
         get() = -field
         private set
+    lateinit var dynamicTriggerThresholdRange: ClosedRange<Float>
+        private set
     var swipeProgressThreshold: Float = 0f
         private set
 
@@ -122,8 +124,10 @@
         staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
         reactivationTriggerThreshold =
                 getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
-        deactivationSwipeTriggerThreshold =
+        deactivationTriggerThreshold =
                 getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
+        dynamicTriggerThresholdRange =
+                reactivationTriggerThreshold..deactivationTriggerThreshold
         swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
 
         entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
@@ -136,7 +140,6 @@
         edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f)
         heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f)
 
-        val entryActiveHorizontalTranslationSpring = createSpring(800f, 0.76f)
         val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f)
         val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f)
         val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f)
@@ -150,7 +153,7 @@
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
                 scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
-                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                horizontalTranslationSpring = createSpring(500f, 0.76f),
                 verticalTranslationSpring = createSpring(30000f, 1f),
                 scaleSpring = createSpring(120f, 0.8f),
                 arrowDimens = ArrowDimens(
@@ -202,7 +205,7 @@
         activeIndicator = BackIndicatorDimens(
                 horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
                 scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
-                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                horizontalTranslationSpring = createSpring(1000f, 0.7f),
                 scaleSpring = createSpring(450f, 0.39f),
                 scalePivotX = getDimen(R.dimen.navigation_edge_active_background_width),
                 arrowDimens = ArrowDimens(
@@ -222,8 +225,8 @@
                         farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners),
                         widthSpring = createSpring(850f, 0.75f),
                         heightSpring = createSpring(10000f, 1f),
-                        edgeCornerRadiusSpring = createSpring(600f, 0.36f),
-                        farCornerRadiusSpring = createSpring(2500f, 0.855f),
+                        edgeCornerRadiusSpring = createSpring(2600f, 0.855f),
+                        farCornerRadiusSpring = createSpring(1200f, 0.30f),
                 )
         )
 
@@ -250,10 +253,10 @@
                                 getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
                         farCornerRadius =
                                 getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
-                        widthSpring = createSpring(250f, 0.65f),
+                        widthSpring = createSpring(400f, 0.65f),
                         heightSpring = createSpring(1500f, 0.45f),
-                        farCornerRadiusSpring = createSpring(200f, 1f),
-                        edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+                        farCornerRadiusSpring = createSpring(300f, 1f),
+                        edgeCornerRadiusSpring = createSpring(250f, 0.5f),
                 )
         )
 
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index 3711a2f..fbf134d 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -103,7 +103,7 @@
     @GuardedBy("callbacks")
     private val callbacks: MutableList<DataItem> = ArrayList()
 
-    fun initialize(startingUser: Int) {
+    open fun initialize(startingUser: Int) {
         if (initialized) {
             return
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 6deef2e..76ff97d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -383,11 +383,11 @@
     }
 
     fun debugString() = buildString {
-        append("TargetView: ${targetView.hashCode()} ")
-        append("Top: $topRoundness ")
-        append(topRoundnessMap.map { "${it.key} ${it.value}" })
-        append(" Bottom: $bottomRoundness ")
-        append(bottomRoundnessMap.map { "${it.key} ${it.value}" })
+        append("Roundable { ")
+        append("top: { value: $topRoundness, requests: $topRoundnessMap}")
+        append(", ")
+        append("bottom: { value: $bottomRoundness, requests: $bottomRoundnessMap}")
+        append("}")
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 0529c94..23b5241 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -38,8 +38,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.dagger.IncomingHeader
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
 import com.android.systemui.statusbar.notification.logKey
 import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
 import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -69,12 +68,12 @@
     private val mSystemClock: SystemClock,
     private val mHeadsUpManager: HeadsUpManager,
     private val mHeadsUpViewBinder: HeadsUpViewBinder,
-    private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
+    private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider,
     private val mRemoteInputManager: NotificationRemoteInputManager,
     private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
     private val mFlags: NotifPipelineFlags,
     @IncomingHeader private val mIncomingHeaderController: NodeController,
-    @Main private val mExecutor: DelayableExecutor,
+    @Main private val mExecutor: DelayableExecutor
 ) : Coordinator {
     private val mEntriesBindingUntil = ArrayMap<String, Long>()
     private val mEntriesUpdateTimes = ArrayMap<String, Long>()
@@ -388,18 +387,21 @@
         override fun onEntryAdded(entry: NotificationEntry) {
             // First check whether this notification should launch a full screen intent, and
             // launch it if needed.
-            val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
-            mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
-            if (fsiDecision.shouldLaunch) {
+            val fsiDecision =
+                mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
+            mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(fsiDecision)
+            if (fsiDecision.shouldInterrupt) {
                 mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
-            } else if (fsiDecision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
+            } else if (fsiDecision.wouldInterruptWithoutDnd) {
                 // If DND was the only reason this entry was suppressed, note it for potential
                 // reconsideration on later ranking updates.
                 addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
             }
 
-            // shouldHeadsUp includes check for whether this notification should be filtered
-            val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
+            // makeAndLogHeadsUpDecision includes check for whether this notification should be
+            // filtered
+            val shouldHeadsUpEver =
+                mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
             mPostedEntries[entry.key] = PostedEntry(
                 entry,
                 wasAdded = true,
@@ -420,7 +422,8 @@
          * up again.
          */
         override fun onEntryUpdated(entry: NotificationEntry) {
-            val shouldHeadsUpEver = mNotificationInterruptStateProvider.shouldHeadsUp(entry)
+            val shouldHeadsUpEver =
+                mVisualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry).shouldInterrupt
             val shouldHeadsUpAgain = shouldHunAgain(entry)
             val isAlerting = mHeadsUpManager.isAlerting(entry.key)
             val isBinding = isEntryBinding(entry)
@@ -510,26 +513,26 @@
                 // If any of these entries are no longer suppressed, launch the FSI now.
                 if (isCandidateForFSIReconsideration(entry)) {
                     val decision =
-                        mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
-                    if (decision.shouldLaunch) {
+                        mVisualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(
+                            entry
+                        )
+                    if (decision.shouldInterrupt) {
                         // Log both the launch of the full screen and also that this was via a
                         // ranking update, and finally revoke candidacy for FSI reconsideration
-                        mLogger.logEntryUpdatedToFullScreen(entry.key, decision.name)
-                        mNotificationInterruptStateProvider.logFullScreenIntentDecision(
-                            entry, decision)
+                        mLogger.logEntryUpdatedToFullScreen(entry.key, decision.logReason)
+                        mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
                         mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
                         mFSIUpdateCandidates.remove(entry.key)
 
                         // if we launch the FSI then this is no longer a candidate for HUN
                         continue
-                    } else if (decision == FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND) {
+                    } else if (decision.wouldInterruptWithoutDnd) {
                         // decision has not changed; no need to log
                     } else {
                         // some other condition is now blocking FSI; log that and revoke candidacy
                         // for FSI reconsideration
-                        mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.name)
-                        mNotificationInterruptStateProvider.logFullScreenIntentDecision(
-                            entry, decision)
+                        mLogger.logEntryDisqualifiedFromFullScreen(entry.key, decision.logReason)
+                        mVisualInterruptionDecisionProvider.logFullScreenIntentDecision(decision)
                         mFSIUpdateCandidates.remove(entry.key)
                     }
                 }
@@ -539,13 +542,18 @@
                 //   state
                 // - if it is present in PostedEntries and the previous state of shouldHeadsUp
                 //   differs from the updated one
-                val shouldHeadsUpEver = mNotificationInterruptStateProvider.checkHeadsUp(entry,
-                                /* log= */ false)
+                val decision =
+                    mVisualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry)
+                val shouldHeadsUpEver = decision.shouldInterrupt
                 val postedShouldHeadsUpEver = mPostedEntries[entry.key]?.shouldHeadsUpEver ?: false
                 val shouldUpdateEntry = postedShouldHeadsUpEver != shouldHeadsUpEver
 
                 if (shouldUpdateEntry) {
-                    mLogger.logEntryUpdatedByRanking(entry.key, shouldHeadsUpEver)
+                    mLogger.logEntryUpdatedByRanking(
+                        entry.key,
+                        shouldHeadsUpEver,
+                        decision.logReason
+                    )
                     onEntryUpdated(entry)
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index e936559..32c3c66 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -61,12 +61,13 @@
         })
     }
 
-    fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean) {
+    fun logEntryUpdatedByRanking(key: String, shouldHun: Boolean, reason: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = key
             bool1 = shouldHun
+            str2 = reason
         }, {
-            "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1"
+            "updating entry via ranking applied: $str1 updated shouldHeadsUp=$bool1 because $str2"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index a352f23..115e0502 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
+import android.util.Log
+
 import com.android.systemui.log.dagger.NotificationInterruptLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
@@ -23,6 +25,7 @@
 import com.android.systemui.plugins.log.LogLevel.WARNING
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NotificationInterruptLogger @Inject constructor(
@@ -44,11 +47,13 @@
     }
 
     fun logNoBubbleNotAllowed(entry: NotificationEntry) {
-        buffer.log(TAG, DEBUG, {
-            str1 = entry.logKey
-        }, {
-            "No bubble up: not allowed to bubble: $str1"
-        })
+        if (Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG)) {
+            buffer.log(TAG, DEBUG, {
+                str1 = entry.logKey
+            }, {
+                "No bubble up: not allowed to bubble: $str1"
+            })
+        }
     }
 
     fun logNoBubbleNoMetadata(entry: NotificationEntry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
index f2216fc..ebba4b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapper.kt
@@ -36,6 +36,8 @@
         SHOULD_INTERRUPT(shouldInterrupt = true),
         SHOULD_NOT_INTERRUPT(shouldInterrupt = false);
 
+        override val logReason = "unknown"
+
         companion object {
             fun of(booleanDecision: Boolean) =
                 if (booleanDecision) SHOULD_INTERRUPT else SHOULD_NOT_INTERRUPT
@@ -49,6 +51,7 @@
     ) : FullScreenIntentDecision {
         override val shouldInterrupt = originalDecision.shouldLaunch
         override val wouldInterruptWithoutDnd = originalDecision == NO_FSI_SUPPRESSED_ONLY_BY_DND
+        override val logReason = originalDecision.name
     }
 
     override fun addSuppressor(suppressor: NotificationInterruptSuppressor) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
index c0f4fcd..8024016 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProvider.kt
@@ -32,9 +32,12 @@
      * full-screen intent decisions.
      *
      * @property[shouldInterrupt] whether a visual interruption should be triggered
+     * @property[logReason] a log-friendly string explaining the reason for the decision; should be
+     *   used *only* for logging, not decision-making
      */
     interface Decision {
         val shouldInterrupt: Boolean
+        val logReason: String
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 10cdae3..e468a59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3649,16 +3649,12 @@
             } else {
                 pw.println("no viewState!!!");
             }
-            pw.println("Roundness: " + getRoundableState().debugString());
+            pw.println(getRoundableState().debugString());
 
             int transientViewCount = mChildrenContainer == null
                     ? 0 : mChildrenContainer.getTransientViewCount();
             if (mIsSummaryWithChildren || transientViewCount > 0) {
-                pw.println();
-                pw.print("ChildrenContainer");
-                pw.print(" visibility: " + mChildrenContainer.getVisibility());
-                pw.print(", alpha: " + mChildrenContainer.getAlpha());
-                pw.print(", translationY: " + mChildrenContainer.getTranslationY());
+                pw.println(mChildrenContainer.debugString());
                 pw.println();
                 List<ExpandableNotificationRow> notificationChildren = getAttachedChildren();
                 pw.print("Children: " + notificationChildren.size() + " {");
@@ -3726,12 +3722,6 @@
     }
 
     @Override
-    public String toString() {
-        String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
-        return "ExpandableNotificationRow:" + hashCode() + " { " + roundableStateDebug + " }";
-    }
-
-    @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
         if (mUseRoundnessSourceTypes) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 197caa2..9aa50e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -352,7 +352,7 @@
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
         super.dump(pw, args);
         DumpUtilsKt.withIncreasedIndent(pw, () -> {
-            pw.println("Roundness: " + getRoundableState().debugString());
+            pw.println(getRoundableState().debugString());
             if (DUMP_VERBOSE) {
                 pw.println("mCustomOutline: " + mCustomOutline + " mOutlineRect: " + mOutlineRect);
                 pw.println("mOutlineAlpha: " + mOutlineAlpha);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
index 49f17b6..6bbeebf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/FooterView.java
@@ -17,8 +17,6 @@
 package com.android.systemui.statusbar.notification.row;
 
 import android.annotation.ColorInt;
-import android.annotation.DrawableRes;
-import android.annotation.StringRes;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -50,8 +48,8 @@
 
     // Footer label
     private TextView mSeenNotifsFooterTextView;
-    private @StringRes int mSeenNotifsFilteredText;
-    private int mUnlockIconSize;
+    private String mSeenNotifsFilteredText;
+    private Drawable mSeenNotifsFilteredIcon;
 
     public FooterView(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -87,30 +85,12 @@
         mManageButton = findViewById(R.id.manage_text);
         mSeenNotifsFooterTextView = findViewById(R.id.unlock_prompt_footer);
         updateResources();
-        updateText();
+        updateContent();
         updateColors();
     }
 
-    public void setFooterLabelTextAndIcon(@StringRes int text, @DrawableRes int icon) {
-        mSeenNotifsFilteredText = text;
-        if (mSeenNotifsFilteredText != 0) {
-            mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
-        } else {
-            mSeenNotifsFooterTextView.setText(null);
-        }
-        Drawable drawable;
-        if (icon == 0) {
-            drawable = null;
-        } else {
-            drawable = getResources().getDrawable(icon);
-            drawable.setBounds(0, 0, mUnlockIconSize, mUnlockIconSize);
-        }
-        mSeenNotifsFooterTextView.setCompoundDrawablesRelative(drawable, null, null, null);
-        updateFooterVisibilityMode();
-    }
-
-    private void updateFooterVisibilityMode() {
-        if (mSeenNotifsFilteredText != 0) {
+    public void setFooterLabelVisible(boolean isVisible) {
+        if (isVisible) {
             mManageButton.setVisibility(View.GONE);
             mClearAllButton.setVisibility(View.GONE);
             mSeenNotifsFooterTextView.setVisibility(View.VISIBLE);
@@ -141,10 +121,10 @@
             return;
         }
         mShowHistory = showHistory;
-        updateText();
+        updateContent();
     }
 
-    private void updateText() {
+    private void updateContent() {
         if (mShowHistory) {
             mManageButton.setText(mManageNotificationHistoryText);
             mManageButton.setContentDescription(mManageNotificationHistoryText);
@@ -152,6 +132,9 @@
             mManageButton.setText(mManageNotificationText);
             mManageButton.setContentDescription(mManageNotificationText);
         }
+        mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
+        mSeenNotifsFooterTextView
+                .setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null);
     }
 
     public boolean isHistoryShown() {
@@ -166,7 +149,7 @@
         mClearAllButton.setContentDescription(
                 mContext.getString(R.string.accessibility_clear_all));
         updateResources();
-        updateText();
+        updateContent();
     }
 
     /**
@@ -190,8 +173,11 @@
         mManageNotificationText = getContext().getString(R.string.manage_notifications_text);
         mManageNotificationHistoryText = getContext()
                 .getString(R.string.manage_notifications_history_text);
-        mUnlockIconSize = getResources()
+        int unlockIconSize = getResources()
                 .getDimensionPixelSize(R.dimen.notifications_unseen_footer_icon_size);
+        mSeenNotifsFilteredText = getContext().getString(R.string.unlock_to_see_notif_text);
+        mSeenNotifsFilteredIcon = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
+        mSeenNotifsFilteredIcon.setBounds(0, 0, unlockIconSize, unlockIconSize);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index c823189..99c6ddb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -33,12 +33,9 @@
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.phone.NotificationIconContainer
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
-import com.android.systemui.util.kotlin.getValue
-import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
 
 /**
  * Controller class for [NotificationShelf]. This implementation serves as a temporary wrapper
@@ -47,39 +44,12 @@
  * removed, this class can go away and the ViewBinder can be used directly.
  */
 @CentralSurfacesScope
-class NotificationShelfViewBinderWrapperControllerImpl
-@Inject
-constructor(
-    private val shelf: NotificationShelf,
-    private val viewModel: NotificationShelfViewModel,
-    featureFlags: FeatureFlags,
-    private val falsingManager: FalsingManager,
-    hostControllerLazy: Lazy<NotificationStackScrollLayoutController>,
-    private val notificationIconAreaController: NotificationIconAreaController,
-) : NotificationShelfController {
-
-    private val hostController: NotificationStackScrollLayoutController by hostControllerLazy
+class NotificationShelfViewBinderWrapperControllerImpl @Inject constructor() :
+    NotificationShelfController {
 
     override val view: NotificationShelf
         get() = unsupported
 
-    init {
-        shelf.apply {
-            setRefactorFlagEnabled(featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR))
-            useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES))
-            setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
-        }
-    }
-
-    fun init() {
-        NotificationShelfViewBinder.bind(viewModel, shelf, falsingManager)
-        hostController.setShelf(shelf)
-        hostController.setOnNotificationRemovedListener { child, _ ->
-            view.requestRoundnessResetFor(child)
-        }
-        notificationIconAreaController.setShelfIcons(shelf.shelfIcons)
-    }
-
     override val intrinsicHeight: Int
         get() = unsupported
 
@@ -99,21 +69,32 @@
         get() = NotificationShelfController.throwIllegalFlagStateError(expected = true)
 }
 
-/** Binds a [NotificationShelf] to its backend. */
+/** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
 object NotificationShelfViewBinder {
     fun bind(
-        viewModel: NotificationShelfViewModel,
         shelf: NotificationShelf,
+        viewModel: NotificationShelfViewModel,
         falsingManager: FalsingManager,
+        featureFlags: FeatureFlags,
+        notificationIconAreaController: NotificationIconAreaController,
     ) {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
-        shelf.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.canModifyColorOfNotifications
-                    .onEach(shelf::setCanModifyColorOfNotifications)
-                    .launchIn(this)
-                viewModel.isClickable.onEach(shelf::setCanInteract).launchIn(this)
-                registerViewListenersWhileAttached(shelf, viewModel)
+        shelf.apply {
+            setRefactorFlagEnabled(true)
+            useRoundnessSourceTypes(featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES))
+            setSensitiveRevealAnimEndabled(featureFlags.isEnabled(Flags.SENSITIVE_REVEAL_ANIM))
+            // TODO(278765923): Replace with eventual NotificationIconContainerViewBinder#bind()
+            notificationIconAreaController.setShelfIcons(shelfIcons)
+            repeatWhenAttached {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        viewModel.canModifyColorOfNotifications.collect(
+                            ::setCanModifyColorOfNotifications
+                        )
+                    }
+                    launch { viewModel.isClickable.collect(::setCanInteract) }
+                    registerViewListenersWhileAttached(shelf, viewModel)
+                }
             }
         }
     }
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 40f55bd..160a230 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
@@ -1546,9 +1546,11 @@
         mUseRoundnessSourceTypes = enabled;
     }
 
-    @Override
-    public String toString() {
-        String roundableStateDebug = "RoundableState = " + getRoundableState().debugString();
-        return "NotificationChildrenContainer:" + hashCode() + " { " + roundableStateDebug + " }";
+    public String debugString() {
+        return TAG + " { "
+                + "visibility: " + getVisibility()
+                + ", alpha: " + getAlpha()
+                + ", translationY: " + getTranslationY()
+                + ", roundableState: " + getRoundableState().debugString() + "}";
     }
 }
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 555d502..5c322d7 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
@@ -2839,6 +2839,7 @@
      * @param listener callback for notification removed
      */
     public void setOnNotificationRemovedListener(OnNotificationRemovedListener listener) {
+        NotificationShelfController.assertRefactorFlagDisabled(mAmbientState.getFeatureFlags());
         mOnNotificationRemovedListener = listener;
     }
 
@@ -2852,10 +2853,14 @@
         if (!mChildTransferInProgress) {
             onViewRemovedInternal(expandableView, this);
         }
-        if (mOnNotificationRemovedListener != null) {
-            mOnNotificationRemovedListener.onNotificationRemoved(
-                    expandableView,
-                    mChildTransferInProgress);
+        if (mAmbientState.getFeatureFlags().isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+            mShelf.requestRoundnessResetFor(expandableView);
+        } else {
+            if (mOnNotificationRemovedListener != null) {
+                mOnNotificationRemovedListener.onNotificationRemoved(
+                        expandableView,
+                        mChildTransferInProgress);
+            }
         }
     }
 
@@ -4741,13 +4746,7 @@
         mFooterView.setVisible(visible, animate);
         mFooterView.setSecondaryVisible(showDismissView, animate);
         mFooterView.showHistory(showHistory);
-        if (mHasFilteredOutSeenNotifications) {
-            mFooterView.setFooterLabelTextAndIcon(
-                    R.string.unlock_to_see_notif_text,
-                    R.drawable.ic_friction_lock_closed);
-        } else {
-            mFooterView.setFooterLabelTextAndIcon(0, 0);
-        }
+        mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
     }
 
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 9979cc4..b69ce38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -105,11 +105,14 @@
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -122,16 +125,17 @@
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.settings.SecureSettings;
 
+import kotlin.Unit;
+
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 
-import kotlin.Unit;
-
 /**
  * Controller for {@link NotificationStackScrollLayout}.
  */
@@ -151,6 +155,8 @@
     private final ConfigurationController mConfigurationController;
     private final ZenModeController mZenModeController;
     private final MetricsLogger mMetricsLogger;
+    private final Optional<NotificationListViewModel> mViewModel;
+
     private final DumpManager mDumpManager;
     private final FalsingCollector mFalsingCollector;
     private final FalsingManager mFalsingManager;
@@ -175,6 +181,8 @@
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final StackStateLogger mStackStateLogger;
     private final NotificationStackScrollLogger mLogger;
+    private final NotificationIconAreaController mNotifIconAreaController;
+
     private final GroupExpansionManager mGroupExpansionManager;
     private final NotifPipelineFlags mNotifPipelineFlags;
     private final SeenNotificationsProvider mSeenNotificationsProvider;
@@ -642,6 +650,7 @@
             KeyguardBypassController keyguardBypassController,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager lockscreenUserManager,
+            Optional<NotificationListViewModel> nsslViewModel,
             MetricsLogger metricsLogger,
             DumpManager dumpManager,
             FalsingCollector falsingCollector,
@@ -665,6 +674,7 @@
             StackStateLogger stackLogger,
             NotificationStackScrollLogger logger,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
+            NotificationIconAreaController notifIconAreaController,
             FeatureFlags featureFlags,
             NotificationTargetsHelper notificationTargetsHelper,
             SecureSettings secureSettings,
@@ -686,6 +696,7 @@
         mKeyguardBypassController = keyguardBypassController;
         mZenModeController = zenModeController;
         mLockscreenUserManager = lockscreenUserManager;
+        mViewModel = nsslViewModel;
         mMetricsLogger = metricsLogger;
         mDumpManager = dumpManager;
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
@@ -707,6 +718,7 @@
         mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator;
         mSeenNotificationsProvider = seenNotificationsProvider;
         mShadeController = shadeController;
+        mNotifIconAreaController = notifIconAreaController;
         mFeatureFlags = featureFlags;
         mUseRoundnessSourceTypes = featureFlags.isEnabled(Flags.USE_ROUNDNESS_SOURCETYPES);
         mNotificationTargetsHelper = notificationTargetsHelper;
@@ -820,6 +832,10 @@
 
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
+
+        mViewModel.ifPresent(
+                vm -> NotificationListViewBinder
+                        .bind(mView, vm, mFalsingManager, mFeatureFlags, mNotifIconAreaController));
     }
 
     private boolean isInVisibleLocation(NotificationEntry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
new file mode 100644
index 0000000..45ae4e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import android.view.LayoutInflater
+import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import com.android.systemui.statusbar.phone.NotificationIconAreaController
+
+/** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
+object NotificationListViewBinder {
+    @JvmStatic
+    fun bind(
+        view: NotificationStackScrollLayout,
+        viewModel: NotificationListViewModel,
+        falsingManager: FalsingManager,
+        featureFlags: FeatureFlags,
+        iconAreaController: NotificationIconAreaController,
+    ) {
+        val shelf =
+            LayoutInflater.from(view.context)
+                .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
+        NotificationShelfViewBinder.bind(
+            shelf,
+            viewModel.shelf,
+            falsingManager,
+            featureFlags,
+            iconAreaController
+        )
+        view.setShelf(shelf)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
new file mode 100644
index 0000000..aab1c2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
+import dagger.Module
+import dagger.Provides
+import java.util.Optional
+import javax.inject.Provider
+
+/** ViewModel for the list of notifications. */
+class NotificationListViewModel(
+    val shelf: NotificationShelfViewModel,
+)
+
+@Module
+object NotificationListViewModelModule {
+    @JvmStatic
+    @Provides
+    fun maybeProvideViewModel(
+        featureFlags: FeatureFlags,
+        shelfViewModel: Provider<NotificationShelfViewModel>,
+    ): Optional<NotificationListViewModel> =
+        if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
+            Optional.of(NotificationListViewModel(shelfViewModel.get()))
+        } else {
+            Optional.empty()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
index fbe374c..c0269b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -23,10 +23,10 @@
 import android.view.WindowInsetsController.Behavior
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -37,7 +37,7 @@
  * It is responsible for modifying any attributes if necessary, and then notifying the other
  * downstream listeners.
  */
-@CentralSurfacesScope
+@SysUISingleton
 class SystemBarAttributesListener
 @Inject
 internal constructor(
@@ -45,18 +45,14 @@
     private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val lightBarController: LightBarController,
-    private val dumpManager: DumpManager,
-) : CentralSurfacesComponent.Startable, StatusBarBoundsProvider.BoundsChangeListener {
+    dumpManager: DumpManager,
+) : Dumpable, StatusBarBoundsProvider.BoundsChangeListener {
 
     private var lastLetterboxAppearance: LetterboxAppearance? = null
     private var lastSystemBarAttributesParams: SystemBarAttributesParams? = null
 
-    override fun start() {
-        dumpManager.registerDumpable(javaClass.simpleName, this::dump)
-    }
-
-    override fun stop() {
-        dumpManager.unregisterDumpable(javaClass.simpleName)
+    init {
+        dumpManager.registerCriticalDumpable(this)
     }
 
     override fun onStatusBarBoundsChanged() {
@@ -128,7 +124,7 @@
     private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) =
         letterboxDetails.isNotEmpty()
 
-    private fun dump(printWriter: PrintWriter, strings: Array<String>) {
+    override fun dump(printWriter: PrintWriter, strings: Array<String>) {
         printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams")
         printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
index f72e74b..7ded90f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
@@ -16,11 +16,7 @@
 
 package com.android.systemui.statusbar.phone.dagger;
 
-import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
-
-import dagger.Binds;
 import dagger.Module;
-import dagger.multibindings.IntoSet;
 import dagger.multibindings.Multibinds;
 
 import java.util.Set;
@@ -29,9 +25,4 @@
 interface CentralSurfacesStartableModule {
     @Multibinds
     Set<CentralSurfacesComponent.Startable> multibindStartables();
-
-    @Binds
-    @IntoSet
-    CentralSurfacesComponent.Startable sysBarAttrsListener(
-            SystemBarAttributesListener systemBarAttributesListener);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
index 95d36b0..ef86162 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java
@@ -55,6 +55,7 @@
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule;
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModelModule;
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
@@ -87,7 +88,10 @@
 import javax.inject.Provider;
 
 @Module(subcomponents = StatusBarFragmentComponent.class,
-        includes = { ActivatableNotificationViewModelModule.class })
+        includes = {
+                ActivatableNotificationViewModelModule.class,
+                NotificationListViewModelModule.class,
+        })
 public abstract class StatusBarViewModule {
 
     public static final String SHADE_HEADER = "large_screen_shade_header";
@@ -117,9 +121,7 @@
             NotificationShelfComponent.Builder notificationShelfComponentBuilder,
             NotificationShelf notificationShelf) {
         if (featureFlags.isEnabled(Flags.NOTIFICATION_SHELF_REFACTOR)) {
-            NotificationShelfViewBinderWrapperControllerImpl impl = newImpl.get();
-            impl.init();
-            return impl;
+            return newImpl.get();
         } else {
             NotificationShelfComponent component = notificationShelfComponentBuilder
                     .notificationShelf(notificationShelf)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt
index 16e1766..be2e41a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt
@@ -45,10 +45,6 @@
     }
 
     companion object {
-        /** Creates a [SignalIconModel] representing an empty and invalidated state. */
-        fun createEmptyState(numberOfLevels: Int) =
-            SignalIconModel(level = 0, numberOfLevels, showExclamationMark = true)
-
         private const val COL_LEVEL = "level"
         private const val COL_NUM_LEVELS = "numLevels"
         private const val COL_SHOW_EXCLAMATION = "showExclamation"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index bfd133e..54730ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
-import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
 import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
@@ -78,13 +77,24 @@
     scope: CoroutineScope,
 ) : MobileIconViewModelCommon {
     /** Whether or not to show the error state of [SignalDrawable] */
-    private val showExclamationMark: Flow<Boolean> =
+    private val showExclamationMark: StateFlow<Boolean> =
         combine(
-            iconInteractor.isDefaultDataEnabled,
-            iconInteractor.isDefaultConnectionFailed,
-        ) { isDefaultDataEnabled, isDefaultConnectionFailed ->
-            !isDefaultDataEnabled || isDefaultConnectionFailed
-        }
+                iconInteractor.isDefaultDataEnabled,
+                iconInteractor.isDefaultConnectionFailed,
+                iconInteractor.isInService,
+            ) { isDefaultDataEnabled, isDefaultConnectionFailed, isInService ->
+                !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), true)
+
+    private val shownLevel: StateFlow<Int> =
+        combine(
+                iconInteractor.level,
+                iconInteractor.isInService,
+            ) { level, isInService ->
+                if (isInService) level else 0
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
 
     override val isVisible: StateFlow<Boolean> =
         if (!constants.hasDataCapabilities) {
@@ -107,18 +117,18 @@
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val icon: Flow<SignalIconModel> = run {
-        val initial = SignalIconModel.createEmptyState(iconInteractor.numberOfLevels.value)
+        val initial =
+            SignalIconModel(
+                level = shownLevel.value,
+                numberOfLevels = iconInteractor.numberOfLevels.value,
+                showExclamationMark = showExclamationMark.value,
+            )
         combine(
-                iconInteractor.level,
+                shownLevel,
                 iconInteractor.numberOfLevels,
                 showExclamationMark,
-                iconInteractor.isInService,
-            ) { level, numberOfLevels, showExclamationMark, isInService ->
-                if (!isInService) {
-                    SignalIconModel.createEmptyState(numberOfLevels)
-                } else {
-                    SignalIconModel(level, numberOfLevels, showExclamationMark)
-                }
+            ) { shownLevel, numberOfLevels, showExclamationMark ->
+                SignalIconModel(shownLevel, numberOfLevels, showExclamationMark)
             }
             .distinctUntilChanged()
             .logDiffsForTable(
@@ -130,19 +140,9 @@
     }
 
     override val contentDescription: Flow<ContentDescription> = run {
-        val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH_NONE)
-        combine(
-                iconInteractor.level,
-                iconInteractor.isInService,
-            ) { level, isInService ->
-                val resId =
-                    when {
-                        isInService -> PHONE_SIGNAL_STRENGTH[level]
-                        else -> PHONE_SIGNAL_STRENGTH_NONE
-                    }
-                ContentDescription.Resource(resId)
-            }
-            .distinctUntilChanged()
+        val initial = ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[0])
+        shownLevel
+            .map { ContentDescription.Resource(PHONE_SIGNAL_STRENGTH[it]) }
             .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index b37c44a..4e52be9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -160,8 +160,10 @@
 
                             val wifi = currentWifi
                             if (
-                                wifi is WifiNetworkModel.Active &&
-                                    wifi.networkId == network.getNetId()
+                                (wifi is WifiNetworkModel.Active &&
+                                    wifi.networkId == network.getNetId()) ||
+                                    (wifi is WifiNetworkModel.CarrierMerged &&
+                                        wifi.networkId == network.getNetId())
                             ) {
                                 val newNetworkModel = WifiNetworkModel.Inactive
                                 currentWifi = newNetworkModel
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
new file mode 100644
index 0000000..1c17fc3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualLocationsService.kt
@@ -0,0 +1,93 @@
+package com.android.systemui.wallet.controller
+
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleService
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Serves as an intermediary between QuickAccessWalletService and ContextualCardManager (in PCC).
+ * When QuickAccessWalletService has a list of store locations, WalletContextualLocationsService
+ * will send them to ContextualCardManager. When the user enters a store location, this Service
+ * class will be notified, and WalletContextualSuggestionsController will be updated.
+ */
+class WalletContextualLocationsService
+@Inject
+constructor(
+    private val controller: WalletContextualSuggestionsController,
+    private val featureFlags: FeatureFlags,
+) : LifecycleService() {
+    private var listener: IWalletCardsUpdatedListener? = null
+    private var scope: CoroutineScope = this.lifecycleScope
+
+    @VisibleForTesting
+    constructor(
+        controller: WalletContextualSuggestionsController,
+        featureFlags: FeatureFlags,
+        scope: CoroutineScope,
+    ) : this(controller, featureFlags) {
+        this.scope = scope
+    }
+
+    override fun onBind(intent: Intent): IBinder {
+        super.onBind(intent)
+        scope.launch {
+            controller.allWalletCards.collect { cards ->
+                val cardsSize = cards.size
+                Log.i(TAG, "Number of cards registered $cardsSize")
+                listener?.registerNewWalletCards(cards)
+            }
+        }
+        return binder
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        listener = null
+    }
+
+    @VisibleForTesting
+    fun addWalletCardsUpdatedListenerInternal(listener: IWalletCardsUpdatedListener) {
+        if (!featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            return
+        }
+        this.listener = listener // Currently, only one listener at a time is supported
+        // Sends WalletCard objects from QuickAccessWalletService to the listener
+        val cards = controller.allWalletCards.value
+        if (!cards.isEmpty()) {
+            val cardsSize = cards.size
+            Log.i(TAG, "Number of cards registered $cardsSize")
+            listener.registerNewWalletCards(cards)
+        }
+    }
+
+    @VisibleForTesting
+    fun onWalletContextualLocationsStateUpdatedInternal(storeLocations: List<String>) {
+        if (!featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
+            return
+        }
+        Log.i(TAG, "Entered store $storeLocations")
+        controller.setSuggestionCardIds(storeLocations.toSet())
+    }
+
+    private val binder: IWalletContextualLocationsService.Stub
+    = object : IWalletContextualLocationsService.Stub() {
+        override fun addWalletCardsUpdatedListener(listener: IWalletCardsUpdatedListener) {
+            addWalletCardsUpdatedListenerInternal(listener)
+        }
+        override fun onWalletContextualLocationsStateUpdated(storeLocations: List<String>) {
+            onWalletContextualLocationsStateUpdatedInternal(storeLocations)
+        }
+    }
+
+    companion object {
+        private const val TAG = "WalletContextualLocationsService"
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
index 518f5a7..b3ad9b0 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallet/controller/WalletContextualSuggestionsController.kt
@@ -36,6 +36,7 @@
 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.emptyFlow
@@ -57,7 +58,8 @@
 ) {
     private val cardsReceivedCallbacks: MutableSet<(List<WalletCard>) -> Unit> = mutableSetOf()
 
-    private val allWalletCards: Flow<List<WalletCard>> =
+    /** All potential cards. */
+    val allWalletCards: StateFlow<List<WalletCard>> =
         if (featureFlags.isEnabled(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS)) {
             // TODO(b/237409756) determine if we should debounce this so we don't call the service
             // too frequently. Also check if the list actually changed before calling callbacks.
@@ -107,12 +109,13 @@
                     emptyList()
                 )
         } else {
-            emptyFlow()
+            MutableStateFlow<List<WalletCard>>(emptyList()).asStateFlow()
         }
 
     private val _suggestionCardIds: MutableStateFlow<Set<String>> = MutableStateFlow(emptySet())
     private val contextualSuggestionsCardIds: Flow<Set<String>> = _suggestionCardIds.asStateFlow()
 
+    /** Contextually-relevant cards. */
     val contextualSuggestionCards: Flow<List<WalletCard>> =
         combine(allWalletCards, contextualSuggestionsCardIds) { cards, ids ->
                 val ret =
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
index 9429d89..efba3e5 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
@@ -35,6 +35,8 @@
 import dagger.multibindings.IntoMap;
 import dagger.multibindings.StringKey;
 
+import android.app.Service;
+import com.android.systemui.wallet.controller.WalletContextualLocationsService;
 
 /**
  * Module for injecting classes in Wallet.
@@ -42,6 +44,12 @@
 @Module
 public abstract class WalletModule {
 
+    @Binds
+    @IntoMap
+    @ClassKey(WalletContextualLocationsService.class)
+    abstract Service bindWalletContextualLocationsService(
+        WalletContextualLocationsService service);
+
     /** */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index a4b093d..a5365fb 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -66,7 +66,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -100,7 +100,7 @@
     private final INotificationManager mNotificationManager;
     private final IDreamManager mDreamManager;
     private final NotificationVisibilityProvider mVisibilityProvider;
-    private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
+    private final VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider;
     private final NotificationLockscreenUserManager mNotifUserManager;
     private final CommonNotifCollection mCommonNotifCollection;
     private final NotifPipeline mNotifPipeline;
@@ -126,7 +126,7 @@
             INotificationManager notificationManager,
             IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
-            NotificationInterruptStateProvider interruptionStateProvider,
+            VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
             CommonNotifCollection notifCollection,
@@ -145,7 +145,7 @@
                     notificationManager,
                     dreamManager,
                     visibilityProvider,
-                    interruptionStateProvider,
+                    visualInterruptionDecisionProvider,
                     zenModeController,
                     notifUserManager,
                     notifCollection,
@@ -169,7 +169,7 @@
             INotificationManager notificationManager,
             IDreamManager dreamManager,
             NotificationVisibilityProvider visibilityProvider,
-            NotificationInterruptStateProvider interruptionStateProvider,
+            VisualInterruptionDecisionProvider visualInterruptionDecisionProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
             CommonNotifCollection notifCollection,
@@ -185,7 +185,7 @@
         mNotificationManager = notificationManager;
         mDreamManager = dreamManager;
         mVisibilityProvider = visibilityProvider;
-        mNotificationInterruptStateProvider = interruptionStateProvider;
+        mVisualInterruptionDecisionProvider = visualInterruptionDecisionProvider;
         mNotifUserManager = notifUserManager;
         mCommonNotifCollection = notifCollection;
         mNotifPipeline = notifPipeline;
@@ -272,7 +272,7 @@
                     for (NotificationEntry entry : activeEntries) {
                         if (mNotifUserManager.isCurrentProfile(entry.getSbn().getUserId())
                                 && savedBubbleKeys.contains(entry.getKey())
-                                && mNotificationInterruptStateProvider.shouldBubbleUp(entry)
+                                && shouldBubbleUp(entry)
                                 && entry.isBubble()) {
                             result.add(notifToBubbleEntry(entry));
                         }
@@ -416,16 +416,13 @@
     }
 
     void onEntryAdded(NotificationEntry entry) {
-        if (mNotificationInterruptStateProvider.shouldBubbleUp(entry)
-                && entry.isBubble()) {
+        if (shouldBubbleUp(entry) && entry.isBubble()) {
             mBubbles.onEntryAdded(notifToBubbleEntry(entry));
         }
     }
 
     void onEntryUpdated(NotificationEntry entry, boolean fromSystem) {
-        boolean shouldBubble = mNotificationInterruptStateProvider.shouldBubbleUp(entry);
-        mBubbles.onEntryUpdated(notifToBubbleEntry(entry),
-                shouldBubble, fromSystem);
+        mBubbles.onEntryUpdated(notifToBubbleEntry(entry), shouldBubbleUp(entry), fromSystem);
     }
 
     void onEntryRemoved(NotificationEntry entry) {
@@ -438,12 +435,8 @@
         for (int i = 0; i < orderedKeys.length; i++) {
             String key = orderedKeys[i];
             final NotificationEntry entry = mCommonNotifCollection.getEntry(key);
-            BubbleEntry bubbleEntry = entry != null
-                    ? notifToBubbleEntry(entry)
-                    : null;
-            boolean shouldBubbleUp = entry != null
-                    ? mNotificationInterruptStateProvider.shouldBubbleUp(entry)
-                    : false;
+            BubbleEntry bubbleEntry = entry != null ? notifToBubbleEntry(entry) : null;
+            boolean shouldBubbleUp = entry != null ? shouldBubbleUp(entry) : false;
             pendingOrActiveNotif.put(key, new Pair<>(bubbleEntry, shouldBubbleUp));
         }
         mBubbles.onRankingUpdated(rankingMap, pendingOrActiveNotif);
@@ -637,6 +630,10 @@
         }
     }
 
+    private boolean shouldBubbleUp(NotificationEntry e) {
+        return mVisualInterruptionDecisionProvider.makeAndLogBubbleDecision(e).getShouldInterrupt();
+    }
+
     /**
      * Callback for when the BubbleController wants to interact with the notification pipeline to:
      * - Remove a previously bubbled notification
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
index 8a5c5b5..57a355f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/FontInterpolatorTest.kt
@@ -106,4 +106,29 @@
         val reversedFont = interp.lerp(endFont, startFont, 0.5f)
         assertThat(resultFont).isSameInstanceAs(reversedFont)
     }
+
+    @Test
+    fun testCacheMaxSize() {
+        val interp = FontInterpolator()
+
+        val startFont = Font.Builder(sFont)
+                .setFontVariationSettings("'wght' 100")
+                .build()
+        val endFont = Font.Builder(sFont)
+                .setFontVariationSettings("'wght' 1")
+                .build()
+        val resultFont = interp.lerp(startFont, endFont, 0.5f)
+        for (i in 0..FONT_CACHE_MAX_ENTRIES + 1) {
+            val f1 = Font.Builder(sFont)
+                    .setFontVariationSettings("'wght' ${i * 100}")
+                    .build()
+            val f2 = Font.Builder(sFont)
+                    .setFontVariationSettings("'wght' $i")
+                    .build()
+            interp.lerp(f1, f2, 0.5f)
+        }
+
+        val cachedFont = interp.lerp(startFont, endFont, 0.5f)
+        assertThat(resultFont).isNotSameInstanceAs(cachedFont)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java
new file mode 100644
index 0000000..362d26b0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.face.FaceManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.ExecutionException;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class BiometricNotificationDialogFactoryTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+
+    @Mock
+    FingerprintManager mFingerprintManager;
+    @Mock
+    FaceManager mFaceManager;
+    @Mock
+    SystemUIDialog mDialog;
+
+    private Context mContextSpy;
+    private final ArgumentCaptor<DialogInterface.OnClickListener> mOnClickListenerArgumentCaptor =
+            ArgumentCaptor.forClass(DialogInterface.OnClickListener.class);
+    private final ArgumentCaptor<Intent> mIntentArgumentCaptor =
+            ArgumentCaptor.forClass(Intent.class);
+    private BiometricNotificationDialogFactory mDialogFactory;
+
+    @Before
+    public void setUp() throws ExecutionException, InterruptedException {
+        mContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
+        mContext.addMockSystemService(FaceManager.class, mFaceManager);
+
+        when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+        when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
+        mContextSpy = spy(mContext);
+        mDialogFactory = new BiometricNotificationDialogFactory();
+    }
+
+    @Test
+    public void testFingerprintReEnrollDialog_onRemovalSucceeded() {
+        mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
+                BiometricSourceType.FINGERPRINT);
+
+        verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture());
+
+        DialogInterface.OnClickListener positiveOnClickListener =
+                mOnClickListenerArgumentCaptor.getValue();
+        positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+        ArgumentCaptor<FingerprintManager.RemovalCallback> removalCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(FingerprintManager.RemovalCallback.class);
+
+        verify(mFingerprintManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture());
+
+        removalCallbackArgumentCaptor.getValue().onRemovalSucceeded(null /* fp */,
+                0 /* remaining */);
+
+        verify(mContextSpy).startActivity(mIntentArgumentCaptor.capture());
+        assertThat(mIntentArgumentCaptor.getValue().getAction()).isEqualTo(
+                Settings.ACTION_FINGERPRINT_ENROLL);
+    }
+
+    @Test
+    public void testFingerprintReEnrollDialog_onRemovalError() {
+        mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
+                BiometricSourceType.FINGERPRINT);
+
+        verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture());
+
+        DialogInterface.OnClickListener positiveOnClickListener =
+                mOnClickListenerArgumentCaptor.getValue();
+        positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+        ArgumentCaptor<FingerprintManager.RemovalCallback> removalCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(FingerprintManager.RemovalCallback.class);
+
+        verify(mFingerprintManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture());
+
+        removalCallbackArgumentCaptor.getValue().onRemovalError(null /* fp */,
+                0 /* errmsgId */, "Error" /* errString */);
+
+        verify(mContextSpy, never()).startActivity(any());
+    }
+
+    @Test
+    public void testFaceReEnrollDialog_onRemovalSucceeded() {
+        mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
+                BiometricSourceType.FACE);
+
+        verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture());
+
+        DialogInterface.OnClickListener positiveOnClickListener =
+                mOnClickListenerArgumentCaptor.getValue();
+        positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+        ArgumentCaptor<FaceManager.RemovalCallback> removalCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(FaceManager.RemovalCallback.class);
+
+        verify(mFaceManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture());
+
+        removalCallbackArgumentCaptor.getValue().onRemovalSucceeded(null /* fp */,
+                0 /* remaining */);
+
+        verify(mContextSpy).startActivity(mIntentArgumentCaptor.capture());
+        assertThat(mIntentArgumentCaptor.getValue().getAction()).isEqualTo(
+                "android.settings.FACE_ENROLL");
+    }
+
+    @Test
+    public void testFaceReEnrollDialog_onRemovalError() {
+        mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
+                BiometricSourceType.FACE);
+
+        verify(mDialog).setPositiveButton(anyInt(), mOnClickListenerArgumentCaptor.capture());
+
+        DialogInterface.OnClickListener positiveOnClickListener =
+                mOnClickListenerArgumentCaptor.getValue();
+        positiveOnClickListener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+        ArgumentCaptor<FaceManager.RemovalCallback> removalCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(FaceManager.RemovalCallback.class);
+
+        verify(mFaceManager).removeAll(anyInt(), removalCallbackArgumentCaptor.capture());
+
+        removalCallbackArgumentCaptor.getValue().onRemovalError(null /* face */,
+                0 /* errmsgId */, "Error" /* errString */);
+
+        verify(mContextSpy, never()).startActivity(any());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java
new file mode 100644
index 0000000..b8bca3a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG;
+import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.hardware.biometrics.BiometricFaceConstants;
+import android.hardware.biometrics.BiometricFingerprintConstants;
+import android.hardware.biometrics.BiometricSourceType;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class BiometricNotificationServiceTest extends SysuiTestCase {
+    @Rule
+    public MockitoRule rule = MockitoJUnit.rule();
+
+    @Mock
+    KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Mock
+    KeyguardStateController mKeyguardStateController;
+    @Mock
+    NotificationManager mNotificationManager;
+
+    private static final String TAG = "BiometricNotificationService";
+    private static final int FACE_NOTIFICATION_ID = 1;
+    private static final int FINGERPRINT_NOTIFICATION_ID = 2;
+    private static final long SHOW_NOTIFICATION_DELAY_MS = 5_000L; // 5 seconds
+
+    private final ArgumentCaptor<Notification> mNotificationArgumentCaptor =
+            ArgumentCaptor.forClass(Notification.class);
+    private TestableLooper mLooper;
+    private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+    private KeyguardStateController.Callback mKeyguardStateControllerCallback;
+
+    @Before
+    public void setUp() {
+        mLooper = TestableLooper.get(this);
+        Handler handler = new Handler(mLooper.getLooper());
+        BiometricNotificationDialogFactory dialogFactory = new BiometricNotificationDialogFactory();
+        BiometricNotificationBroadcastReceiver broadcastReceiver =
+                new BiometricNotificationBroadcastReceiver(mContext, dialogFactory);
+        BiometricNotificationService biometricNotificationService =
+                new BiometricNotificationService(mContext,
+                        mKeyguardUpdateMonitor, mKeyguardStateController, handler,
+                        mNotificationManager,
+                        broadcastReceiver);
+        biometricNotificationService.start();
+
+        ArgumentCaptor<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+        ArgumentCaptor<KeyguardStateController.Callback> stateControllerCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+
+        verify(mKeyguardUpdateMonitor).registerCallback(
+                updateMonitorCallbackArgumentCaptor.capture());
+        verify(mKeyguardStateController).addCallback(
+                stateControllerCallbackArgumentCaptor.capture());
+
+        mKeyguardUpdateMonitorCallback = updateMonitorCallbackArgumentCaptor.getValue();
+        mKeyguardStateControllerCallback = stateControllerCallbackArgumentCaptor.getValue();
+    }
+
+    @Test
+    public void testShowFingerprintReEnrollNotification() {
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+
+        mKeyguardUpdateMonitorCallback.onBiometricError(
+                BiometricFingerprintConstants.BIOMETRIC_ERROR_RE_ENROLL,
+                "Testing Fingerprint Re-enrollment" /* errString */,
+                BiometricSourceType.FINGERPRINT
+        );
+        mKeyguardStateControllerCallback.onKeyguardShowingChanged();
+
+        mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS);
+        mLooper.processAllMessages();
+
+        verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FINGERPRINT_NOTIFICATION_ID),
+                mNotificationArgumentCaptor.capture(), any());
+
+        Notification fingerprintNotification = mNotificationArgumentCaptor.getValue();
+
+        assertThat(fingerprintNotification.contentIntent.getIntent().getAction())
+                .isEqualTo(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG);
+    }
+    @Test
+    public void testShowFaceReEnrollNotification() {
+        when(mKeyguardStateController.isShowing()).thenReturn(false);
+
+        mKeyguardUpdateMonitorCallback.onBiometricError(
+                BiometricFaceConstants.BIOMETRIC_ERROR_RE_ENROLL,
+                "Testing Face Re-enrollment" /* errString */,
+                BiometricSourceType.FACE
+        );
+        mKeyguardStateControllerCallback.onKeyguardShowingChanged();
+
+        mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS);
+        mLooper.processAllMessages();
+
+        verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FACE_NOTIFICATION_ID),
+                mNotificationArgumentCaptor.capture(), any());
+
+        Notification fingerprintNotification = mNotificationArgumentCaptor.getValue();
+
+        assertThat(fingerprintNotification.contentIntent.getIntent().getAction())
+                .isEqualTo(ACTION_SHOW_FACE_REENROLL_DIALOG);
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
index f6ff4b2..6f9dedf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
@@ -96,6 +96,16 @@
         }
 
     @Test
+    fun emitsDisconnected_whenDeviceWithIdDoesNotExist() =
+        testScope.runTest {
+            val deviceListener = captureDeviceListener()
+            val isKeyboardConnected by collectLastValue(underTest.keyboardConnected)
+
+            deviceListener.onInputDeviceAdded(NULL_DEVICE_ID)
+            assertThat(isKeyboardConnected).isFalse()
+        }
+
+    @Test
     fun emitsDisconnected_whenKeyboardDisconnects() =
         testScope.runTest {
             val deviceListener = captureDeviceListener()
@@ -172,6 +182,7 @@
         private const val VIRTUAL_FULL_KEYBOARD_ID = 2
         private const val PHYSICAL_NOT_FULL_KEYBOARD_ID = 3
         private const val ANOTHER_PHYSICAL_FULL_KEYBOARD_ID = 4
+        private const val NULL_DEVICE_ID = 5
 
         private val INPUT_DEVICES_MAP: Map<Int, InputDevice> =
             mapOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index 8e32f81..d9428f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -124,7 +124,7 @@
         continueTouch(START_X + touchSlop.toFloat() + 1)
         continueTouch(
             START_X + touchSlop + triggerThreshold -
-                mBackPanelController.params.deactivationSwipeTriggerThreshold
+                mBackPanelController.params.deactivationTriggerThreshold
         )
         clearInvocations(backCallback)
         Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 67128ff..283efe2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -38,8 +38,10 @@
 import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider
 import com.android.systemui.statusbar.notification.collection.render.NodeController
 import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
 import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback
 import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
 import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -52,6 +54,7 @@
 import com.android.systemui.util.time.FakeSystemClock
 import java.util.ArrayList
 import java.util.function.Consumer
+import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -86,7 +89,7 @@
     private val logger = HeadsUpCoordinatorLogger(logcatLogBuffer(), verbose = true)
     private val headsUpManager: HeadsUpManager = mock()
     private val headsUpViewBinder: HeadsUpViewBinder = mock()
-    private val notificationInterruptStateProvider: NotificationInterruptStateProvider = mock()
+    private val visualInterruptionDecisionProvider: VisualInterruptionDecisionProvider = mock()
     private val remoteInputManager: NotificationRemoteInputManager = mock()
     private val endLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
     private val headerController: NodeController = mock()
@@ -114,7 +117,7 @@
             systemClock,
             headsUpManager,
             headsUpViewBinder,
-            notificationInterruptStateProvider,
+            visualInterruptionDecisionProvider,
             remoteInputManager,
             launchFullScreenIntentProvider,
             flags,
@@ -168,8 +171,11 @@
         groupChild2 = helper.createChildNotification(GROUP_ALERT_ALL, 2, "child", 250)
         groupChild3 = helper.createChildNotification(GROUP_ALERT_ALL, 3, "child", 150)
 
+        // Set the default HUN decision
+        setDefaultShouldHeadsUp(false)
+
         // Set the default FSI decision
-        setShouldFullScreen(any(), FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
+        setDefaultShouldFullScreen(FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
     }
 
     @Test
@@ -1006,31 +1012,59 @@
         verify(launchFullScreenIntentProvider, never()).launchFullScreenIntent(entry)
     }
 
-    private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
-        whenever(notificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
-        whenever(notificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
-                .thenReturn(should)
+    private fun setDefaultShouldHeadsUp(should: Boolean) {
+        whenever(visualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(any()))
+            .thenReturn(DecisionImpl.of(should))
+        whenever(visualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(any()))
+            .thenReturn(DecisionImpl.of(should))
     }
 
-    private fun setShouldFullScreen(entry: NotificationEntry, decision: FullScreenIntentDecision) {
-        whenever(notificationInterruptStateProvider.getFullScreenIntentDecision(entry))
-            .thenReturn(decision)
+    private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
+        whenever(visualInterruptionDecisionProvider.makeAndLogHeadsUpDecision(entry))
+            .thenReturn(DecisionImpl.of(should))
+        whenever(visualInterruptionDecisionProvider.makeUnloggedHeadsUpDecision(entry))
+            .thenReturn(DecisionImpl.of(should))
+    }
+
+    private fun setDefaultShouldFullScreen(
+        originalDecision: FullScreenIntentDecision
+    ) {
+        val provider = visualInterruptionDecisionProvider
+        whenever(provider.makeUnloggedFullScreenIntentDecision(any())).thenAnswer {
+            val entry: NotificationEntry = it.getArgument(0)
+            FullScreenIntentDecisionImpl(entry, originalDecision)
+        }
+    }
+
+    private fun setShouldFullScreen(
+        entry: NotificationEntry,
+        originalDecision: FullScreenIntentDecision
+    ) {
+        whenever(
+            visualInterruptionDecisionProvider.makeUnloggedFullScreenIntentDecision(entry)
+        ).thenAnswer {
+            FullScreenIntentDecisionImpl(entry, originalDecision)
+        }
     }
 
     private fun verifyLoggedFullScreenIntentDecision(
         entry: NotificationEntry,
-        decision: FullScreenIntentDecision
+        originalDecision: FullScreenIntentDecision
     ) {
-        verify(notificationInterruptStateProvider).logFullScreenIntentDecision(entry, decision)
+        val decision = withArgCaptor {
+            verify(visualInterruptionDecisionProvider).logFullScreenIntentDecision(capture())
+        }
+        check(decision is FullScreenIntentDecisionImpl)
+        assertEquals(entry, decision.originalEntry)
+        assertEquals(originalDecision, decision.originalDecision)
     }
 
     private fun verifyNoFullScreenIntentDecisionLogged() {
-        verify(notificationInterruptStateProvider, never())
-            .logFullScreenIntentDecision(any(), any())
+        verify(visualInterruptionDecisionProvider, never()).logFullScreenIntentDecision(any())
     }
 
     private fun clearInterruptionProviderInvocations() {
-        clearInvocations(notificationInterruptStateProvider)
+        clearInvocations(visualInterruptionDecisionProvider)
     }
 
     private fun finishBind(entry: NotificationEntry) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
index 819a75b..90cb734 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FooterViewTest.java
@@ -24,12 +24,12 @@
 
 import static org.mockito.Mockito.mock;
 
+import android.testing.AndroidTestingRunner;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.TextView;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
@@ -39,7 +39,7 @@
 import org.junit.runner.RunWith;
 
 @SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
 public class FooterViewTest extends SysuiTestCase {
 
     FooterView mView;
@@ -102,14 +102,21 @@
     }
 
     @Test
-    public void testSetFooterLabelTextAndIcon() {
-        mView.setFooterLabelTextAndIcon(
-                R.string.unlock_to_see_notif_text,
-                R.drawable.ic_friction_lock_closed);
+    public void testSetFooterLabelVisible() {
+        mView.setFooterLabelVisible(true);
         assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.GONE);
         assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.GONE);
         assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility())
                 .isEqualTo(View.VISIBLE);
     }
+
+    @Test
+    public void testSetFooterLabelInvisible() {
+        mView.setFooterLabelVisible(false);
+        assertThat(mView.findViewById(R.id.manage_text).getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mView.findSecondaryView().getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mView.findViewById(R.id.unlock_prompt_footer).getVisibility())
+                .isEqualTo(View.GONE);
+    }
 }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 5425d76..420c7ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -72,9 +72,11 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -92,6 +94,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests for {@link NotificationStackScrollLayoutController}.
  */
@@ -138,6 +142,7 @@
     @Mock private FeatureFlags mFeatureFlags;
     @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
     @Mock private SecureSettings mSecureSettings;
+    @Mock private NotificationIconAreaController mIconAreaController;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -458,6 +463,7 @@
                 mKeyguardBypassController,
                 mZenModeController,
                 mNotificationLockscreenUserManager,
+                Optional.<NotificationListViewModel>empty(),
                 mMetricsLogger,
                 mDumpManager,
                 new FalsingCollectorFake(),
@@ -481,6 +487,7 @@
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
+                mIconAreaController,
                 mFeatureFlags,
                 mNotificationTargetsHelper,
                 mSecureSettings,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index d30e024..dc68180 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -994,7 +994,7 @@
         }
 
     @Test
-    fun wifiNetwork_currentNetworkLost_flowHasNoNetwork() =
+    fun wifiNetwork_currentActiveNetworkLost_flowHasNoNetwork() =
         testScope.runTest {
             var latest: WifiNetworkModel? = null
             val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
@@ -1012,6 +1012,33 @@
             job.cancel()
         }
 
+    /** Possible regression test for b/278618530. */
+    @Test
+    fun wifiNetwork_currentCarrierMergedNetworkLost_flowHasNoNetwork() =
+        testScope.runTest {
+            var latest: WifiNetworkModel? = null
+            val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this)
+
+            val wifiInfo =
+                mock<WifiInfo>().apply {
+                    whenever(this.isPrimary).thenReturn(true)
+                    whenever(this.isCarrierMerged).thenReturn(true)
+                }
+
+            getNetworkCallback()
+                .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+            assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+            assertThat((latest as WifiNetworkModel.CarrierMerged).networkId).isEqualTo(NETWORK_ID)
+
+            // WHEN we lose our current network
+            getNetworkCallback().onLost(NETWORK)
+
+            // THEN we update to no network
+            assertThat(latest is WifiNetworkModel.Inactive).isTrue()
+
+            job.cancel()
+        }
+
     @Test
     fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() =
         testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt
new file mode 100644
index 0000000..af1d788
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/controller/WalletContextualLocationsServiceTest.kt
@@ -0,0 +1,128 @@
+package com.android.systemui.wallet.controller
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.os.Looper
+import android.service.quickaccesswallet.WalletCard
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.anySet
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(JUnit4::class)
+@SmallTest
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class WalletContextualLocationsServiceTest : SysuiTestCase() {
+    @Mock private lateinit var controller: WalletContextualSuggestionsController
+    private var featureFlags = FakeFeatureFlags()
+    private lateinit var underTest: WalletContextualLocationsService
+    private lateinit var testScope: TestScope
+    private var listenerRegisteredCount: Int = 0
+    private val listener: IWalletCardsUpdatedListener.Stub = object : IWalletCardsUpdatedListener.Stub() {
+        override fun registerNewWalletCards(cards: List<WalletCard?>) {
+            listenerRegisteredCount++
+        }
+    }
+
+    @Before
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        doReturn(fakeWalletCards).whenever(controller).allWalletCards
+        doNothing().whenever(controller).setSuggestionCardIds(anySet())
+
+        if (Looper.myLooper() == null) Looper.prepare()
+
+        testScope = TestScope()
+        featureFlags.set(Flags.ENABLE_WALLET_CONTEXTUAL_LOYALTY_CARDS, true)
+        listenerRegisteredCount = 0
+
+        underTest = WalletContextualLocationsService(controller, featureFlags, testScope.backgroundScope)
+    }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun addListener() = testScope.runTest {
+        underTest.addWalletCardsUpdatedListenerInternal(listener)
+        assertThat(listenerRegisteredCount).isEqualTo(1)
+  }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun addStoreLocations() = testScope.runTest {
+        underTest.onWalletContextualLocationsStateUpdatedInternal(ArrayList<String>())
+        verify(controller, times(1)).setSuggestionCardIds(anySet())
+    }
+
+    @Test
+    @kotlinx.coroutines.ExperimentalCoroutinesApi
+    fun updateListenerAndLocationsState() = testScope.runTest {
+        // binds to the service and adds a listener
+        val underTestStub = getInterface
+        underTestStub.addWalletCardsUpdatedListener(listener)
+        assertThat(listenerRegisteredCount).isEqualTo(1)
+
+        // sends a list of card IDs to the controller
+        underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>())
+        verify(controller, times(1)).setSuggestionCardIds(anySet())
+
+        // adds another listener
+        fakeWalletCards.update{ updatedFakeWalletCards }
+        runCurrent()
+        assertThat(listenerRegisteredCount).isEqualTo(2)
+
+        // sends another list of card IDs to the controller
+        underTestStub.onWalletContextualLocationsStateUpdated(ArrayList<String>())
+        verify(controller, times(2)).setSuggestionCardIds(anySet())
+    }
+
+    private val fakeWalletCards: MutableStateFlow<List<WalletCard>>
+        get() {
+            val intent = Intent(getContext(), WalletContextualLocationsService::class.java)
+            val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888))
+            val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>()
+            walletCards.add(WalletCard.Builder("card1", icon, "card", pi).build())
+            walletCards.add(WalletCard.Builder("card2", icon, "card", pi).build())
+            return MutableStateFlow<List<WalletCard>>(walletCards)
+        }
+
+    private val updatedFakeWalletCards: List<WalletCard>
+        get() {
+            val intent = Intent(getContext(), WalletContextualLocationsService::class.java)
+            val pi: PendingIntent = PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            val icon: Icon = Icon.createWithBitmap(Bitmap.createBitmap(70, 50, Bitmap.Config.ARGB_8888))
+            val walletCards: ArrayList<WalletCard> = ArrayList<WalletCard>()
+            walletCards.add(WalletCard.Builder("card3", icon, "card", pi).build())
+            return walletCards
+        }
+
+    private val getInterface: IWalletContextualLocationsService
+        get() {
+            val intent = Intent()
+            return IWalletContextualLocationsService.Stub.asInterface(underTest.onBind(intent))
+        }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index bc3a5b7..1510ee8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -115,6 +115,7 @@
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -398,7 +399,7 @@
                 mock(INotificationManager.class),
                 mIDreamManager,
                 mVisibilityProvider,
-                interruptionStateProvider,
+                new NotificationInterruptStateProviderWrapper(interruptionStateProvider),
                 mZenModeController,
                 mLockscreenUserManager,
                 mCommonNotifCollection,
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 9a257e5..777c7c8 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1417,20 +1417,29 @@
                     mSendTouchExplorationEndDelayed.forceSendAndRemove();
                 }
             }
-            if (!mState.isTouchInteracting()) {
+            if (!mState.isTouchInteracting() && !mState.isDragging()) {
                 // It makes no sense to delegate.
-                Slog.e(LOG_TAG, "Error: Trying to delegate from "
-                        + mState.getStateSymbolicName(mState.getState()));
+                Slog.e(
+                        LOG_TAG,
+                        "Error: Trying to delegate from "
+                                + mState.getStateSymbolicName(mState.getState()));
                 return;
             }
-            mState.startDelegating();
-            MotionEvent prototype = mState.getLastReceivedEvent();
-            if (prototype == null) {
+            MotionEvent event = mState.getLastReceivedEvent();
+            MotionEvent rawEvent = mState.getLastReceivedRawEvent();
+            if (event == null || rawEvent == null) {
                 Slog.d(LOG_TAG, "Unable to start delegating: unable to get last received event.");
                 return;
             }
             int policyFlags = mState.getLastReceivedPolicyFlags();
-            mDispatcher.sendDownForAllNotInjectedPointers(prototype, policyFlags);
+            if (mState.isDragging()) {
+                // Send an event to the end of the drag gesture.
+                mDispatcher.sendMotionEvent(
+                        event, ACTION_UP, rawEvent, ALL_POINTER_ID_BITS, policyFlags);
+            }
+            mState.startDelegating();
+            // Deliver all pointers to the view hierarchy.
+            mDispatcher.sendDownForAllNotInjectedPointers(event, policyFlags);
         }
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java
index 16d2e6b..93531dd 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/AlwaysOnMagnificationFeatureFlag.java
@@ -16,76 +16,31 @@
 
 package com.android.server.accessibility.magnification;
 
-import android.annotation.NonNull;
 import android.provider.DeviceConfig;
 
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.concurrent.Executor;
-
 /**
  * Encapsulates the feature flags for always on magnification. {@see DeviceConfig}
  *
  * @hide
  */
-public class AlwaysOnMagnificationFeatureFlag {
+public class AlwaysOnMagnificationFeatureFlag extends MagnificationFeatureFlagBase {
 
     private static final String NAMESPACE = DeviceConfig.NAMESPACE_WINDOW_MANAGER;
     private static final String FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION =
             "AlwaysOnMagnifier__enable_always_on_magnifier";
 
-    private AlwaysOnMagnificationFeatureFlag() {}
-
-    /** Returns true if the feature flag is enabled for always on magnification */
-    public static boolean isAlwaysOnMagnificationEnabled() {
-        return DeviceConfig.getBoolean(
-                NAMESPACE,
-                FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION,
-                /* defaultValue= */ false);
+    @Override
+    String getNamespace() {
+        return NAMESPACE;
     }
 
-    /** Sets the feature flag. Only used for testing; requires shell permissions. */
-    @VisibleForTesting
-    public static boolean setAlwaysOnMagnificationEnabled(boolean isEnabled) {
-        return DeviceConfig.setProperty(
-                NAMESPACE,
-                FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION,
-                Boolean.toString(isEnabled),
-                /* makeDefault= */ false);
+    @Override
+    String getFeatureName() {
+        return FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION;
     }
 
-    /**
-     * Adds a listener for when the feature flag changes.
-     *
-     * <p>{@see DeviceConfig#addOnPropertiesChangedListener(
-     * String, Executor, DeviceConfig.OnPropertiesChangedListener)}
-     */
-    @NonNull
-    public static DeviceConfig.OnPropertiesChangedListener addOnChangedListener(
-            @NonNull Executor executor, @NonNull Runnable listener) {
-        DeviceConfig.OnPropertiesChangedListener onChangedListener =
-                properties -> {
-                    if (properties.getKeyset().contains(
-                            FEATURE_NAME_ENABLE_ALWAYS_ON_MAGNIFICATION)) {
-                        listener.run();
-                    }
-                };
-        DeviceConfig.addOnPropertiesChangedListener(
-                NAMESPACE,
-                executor,
-                onChangedListener);
-
-        return onChangedListener;
-    }
-
-    /**
-     * Remove a listener for when the feature flag changes.
-     *
-     * <p>{@see DeviceConfig#addOnPropertiesChangedListener(String, Executor,
-     * DeviceConfig.OnPropertiesChangedListener)}
-     */
-    public static void removeOnChangedListener(
-            @NonNull DeviceConfig.OnPropertiesChangedListener onChangedListener) {
-        DeviceConfig.removeOnPropertiesChangedListener(onChangedListener);
+    @Override
+    boolean getDefaultValue() {
+        return false;
     }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index ed8a35f..fbc7b3c 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -38,7 +38,6 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Handler;
 import android.os.Message;
-import android.provider.DeviceConfig;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.MathUtils;
@@ -57,6 +56,7 @@
 import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ConcurrentUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
 import com.android.server.LocalServices;
 import com.android.server.accessibility.AccessibilityManagerService;
@@ -110,6 +110,7 @@
     private boolean mAlwaysOnMagnificationEnabled = false;
     private final DisplayManagerInternal mDisplayManagerInternal;
 
+    private final MagnificationThumbnailFeatureFlag mMagnificationThumbnailFeatureFlag;
     @NonNull private final Supplier<MagnificationThumbnail> mThumbnailSupplier;
 
     /**
@@ -177,9 +178,7 @@
                     mDisplayId, mMagnificationRegion);
             mMagnificationRegion.getBounds(mMagnificationBounds);
 
-            if (mMagnificationThumbnail == null) {
-                mMagnificationThumbnail = mThumbnailSupplier.get();
-            }
+            createThumbnailIfSupported();
 
             return true;
         }
@@ -207,7 +206,7 @@
                 mRegistered = false;
                 unregisterCallbackLocked(mDisplayId, delete);
 
-                destroyThumbNail();
+                destroyThumbnail();
             }
             mUnregisterPending = false;
         }
@@ -345,7 +344,7 @@
                     mMagnificationRegion.set(magnified);
                     mMagnificationRegion.getBounds(mMagnificationBounds);
 
-                    refreshThumbNail(getScale(), getCenterX(), getCenterY());
+                    refreshThumbnail(getScale(), getCenterX(), getCenterY());
 
                     // It's possible that our magnification spec is invalid with the new bounds.
                     // Adjust the current spec's offsets if necessary.
@@ -405,9 +404,9 @@
             }
 
             if (isActivated()) {
-                updateThumbNail(scale, centerX, centerY);
+                updateThumbnail(scale, centerX, centerY);
             } else {
-                hideThumbNail();
+                hideThumbnail();
             }
         }
 
@@ -538,7 +537,7 @@
             mIdOfLastServiceToMagnify = INVALID_SERVICE_ID;
             sendSpecToAnimation(spec, animationCallback);
 
-            hideThumbNail();
+            hideThumbnail();
 
             return changed;
         }
@@ -596,16 +595,16 @@
         }
 
         @GuardedBy("mLock")
-        void updateThumbNail(float scale, float centerX, float centerY) {
+        void updateThumbnail(float scale, float centerX, float centerY) {
             if (mMagnificationThumbnail != null) {
-                mMagnificationThumbnail.updateThumbNail(scale, centerX, centerY);
+                mMagnificationThumbnail.updateThumbnail(scale, centerX, centerY);
             }
         }
 
         @GuardedBy("mLock")
-        void refreshThumbNail(float scale, float centerX, float centerY) {
+        void refreshThumbnail(float scale, float centerX, float centerY) {
             if (mMagnificationThumbnail != null) {
-                mMagnificationThumbnail.setThumbNailBounds(
+                mMagnificationThumbnail.setThumbnailBounds(
                         mMagnificationBounds,
                         scale,
                         centerX,
@@ -615,20 +614,38 @@
         }
 
         @GuardedBy("mLock")
-        void hideThumbNail() {
+        void hideThumbnail() {
             if (mMagnificationThumbnail != null) {
-                mMagnificationThumbnail.hideThumbNail();
+                mMagnificationThumbnail.hideThumbnail();
             }
         }
 
         @GuardedBy("mLock")
-        void destroyThumbNail() {
+        void createThumbnailIfSupported() {
+            if (mMagnificationThumbnail == null) {
+                mMagnificationThumbnail = mThumbnailSupplier.get();
+                // We call refreshThumbnail when the thumbnail is just created to set current
+                // magnification bounds to thumbnail. It to prevent the thumbnail size has not yet
+                // updated properly and thus shows with huge size. (b/276314641)
+                refreshThumbnail(getScale(), getCenterX(), getCenterY());
+            }
+        }
+
+        @GuardedBy("mLock")
+        void destroyThumbnail() {
             if (mMagnificationThumbnail != null) {
-                hideThumbNail();
+                hideThumbnail();
                 mMagnificationThumbnail = null;
             }
         }
 
+        void onThumbnailFeatureFlagChanged() {
+            synchronized (mLock) {
+                destroyThumbnail();
+                createThumbnailIfSupported();
+            }
+        }
+
         /**
          * Updates the current magnification spec.
          *
@@ -768,20 +785,7 @@
                 lock,
                 magnificationInfoChangedCallback,
                 scaleProvider,
-                () -> {
-                    if (DeviceConfig.getBoolean(
-                            DeviceConfig.NAMESPACE_ACCESSIBILITY,
-                            "enable_magnifier_thumbnail",
-                            /* defaultValue= */ false)) {
-                        return new MagnificationThumbnail(
-                            context,
-                            context.getSystemService(WindowManager.class),
-                            new Handler(context.getMainLooper())
-                        );
-                    }
-
-                    return null;
-                });
+                /* thumbnailSupplier= */ null);
     }
 
     /** Constructor for tests */
@@ -791,7 +795,7 @@
             @NonNull Object lock,
             @NonNull MagnificationInfoChangedCallback magnificationInfoChangedCallback,
             @NonNull MagnificationScaleProvider scaleProvider,
-            @NonNull Supplier<MagnificationThumbnail> thumbnailSupplier) {
+            Supplier<MagnificationThumbnail> thumbnailSupplier) {
         mControllerCtx = ctx;
         mLock = lock;
         mMainThreadId = mControllerCtx.getContext().getMainLooper().getThread().getId();
@@ -799,7 +803,41 @@
         addInfoChangedCallback(magnificationInfoChangedCallback);
         mScaleProvider = scaleProvider;
         mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
-        mThumbnailSupplier = thumbnailSupplier;
+        mMagnificationThumbnailFeatureFlag = new MagnificationThumbnailFeatureFlag();
+        mMagnificationThumbnailFeatureFlag.addOnChangedListener(
+                ConcurrentUtils.DIRECT_EXECUTOR, this::onMagnificationThumbnailFeatureFlagChanged);
+        if (thumbnailSupplier != null) {
+            mThumbnailSupplier = thumbnailSupplier;
+        } else {
+            mThumbnailSupplier = () -> {
+                if (mMagnificationThumbnailFeatureFlag.isFeatureFlagEnabled()) {
+                    return new MagnificationThumbnail(
+                            ctx.getContext(),
+                            ctx.getContext().getSystemService(WindowManager.class),
+                            new Handler(ctx.getContext().getMainLooper())
+                    );
+                }
+                return null;
+            };
+        }
+    }
+
+    private void onMagnificationThumbnailFeatureFlagChanged() {
+        synchronized (mLock) {
+            for (int i = 0; i < mDisplays.size(); i++) {
+                onMagnificationThumbnailFeatureFlagChanged(mDisplays.keyAt(i));
+            }
+        }
+    }
+
+    private void onMagnificationThumbnailFeatureFlagChanged(int displayId) {
+        synchronized (mLock) {
+            final DisplayMagnification display = mDisplays.get(displayId);
+            if (display == null) {
+                return;
+            }
+            display.onThumbnailFeatureFlagChanged();
+        }
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index c1c47f5..7ee72df 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -93,6 +93,7 @@
     private final SparseArray<DisableMagnificationCallback>
             mMagnificationEndRunnableSparseArray = new SparseArray();
 
+    private final AlwaysOnMagnificationFeatureFlag mAlwaysOnMagnificationFeatureFlag;
     private final MagnificationScaleProvider mScaleProvider;
     private FullScreenMagnificationController mFullScreenMagnificationController;
     private WindowMagnificationManager mWindowMagnificationMgr;
@@ -151,7 +152,8 @@
         mSupportWindowMagnification = context.getPackageManager().hasSystemFeature(
                 FEATURE_WINDOW_MAGNIFICATION);
 
-        AlwaysOnMagnificationFeatureFlag.addOnChangedListener(
+        mAlwaysOnMagnificationFeatureFlag = new AlwaysOnMagnificationFeatureFlag();
+        mAlwaysOnMagnificationFeatureFlag.addOnChangedListener(
                 ConcurrentUtils.DIRECT_EXECUTOR, mAms::updateAlwaysOnMagnification);
     }
 
@@ -710,7 +712,7 @@
     }
 
     public boolean isAlwaysOnMagnificationFeatureFlagEnabled() {
-        return AlwaysOnMagnificationFeatureFlag.isAlwaysOnMagnificationEnabled();
+        return mAlwaysOnMagnificationFeatureFlag.isFeatureFlagEnabled();
     }
 
     private DisableMagnificationCallback getDisableMagnificationEndRunnableLocked(
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationFeatureFlagBase.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationFeatureFlagBase.java
new file mode 100644
index 0000000..2965887
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationFeatureFlagBase.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.magnification;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.provider.DeviceConfig;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Abstract base class to encapsulates the feature flags for magnification features.
+ * {@see DeviceConfig}
+ *
+ * @hide
+ */
+abstract class MagnificationFeatureFlagBase {
+
+    abstract String getNamespace();
+    abstract String getFeatureName();
+    abstract boolean getDefaultValue();
+
+    private void clearCallingIdentifyAndTryCatch(Runnable tryBlock, Runnable catchBlock) {
+        try {
+            Binder.withCleanCallingIdentity(() -> tryBlock.run());
+        } catch (Throwable throwable) {
+            catchBlock.run();
+        }
+    }
+
+    /** Returns true iff the feature flag is readable and enabled */
+    public boolean isFeatureFlagEnabled() {
+        AtomicBoolean isEnabled = new AtomicBoolean(getDefaultValue());
+
+        clearCallingIdentifyAndTryCatch(
+                () -> isEnabled.set(DeviceConfig.getBoolean(
+                        getNamespace(),
+                        getFeatureName(),
+                        getDefaultValue())),
+                () -> isEnabled.set(getDefaultValue()));
+
+        return isEnabled.get();
+    }
+
+    /** Sets the feature flag. Only used for testing; requires shell permissions. */
+    @VisibleForTesting
+    public boolean setFeatureFlagEnabled(boolean isEnabled) {
+        AtomicBoolean success = new AtomicBoolean(getDefaultValue());
+
+        clearCallingIdentifyAndTryCatch(
+                () -> success.set(DeviceConfig.setProperty(
+                        getNamespace(),
+                        getFeatureName(),
+                        Boolean.toString(isEnabled),
+                        /* makeDefault= */ false)),
+                () -> success.set(getDefaultValue()));
+
+        return success.get();
+    }
+
+    /**
+     * Adds a listener for when the feature flag changes.
+     *
+     * <p>{@see DeviceConfig#addOnPropertiesChangedListener(
+     * String, Executor, DeviceConfig.OnPropertiesChangedListener)}
+     */
+    @NonNull
+    public DeviceConfig.OnPropertiesChangedListener addOnChangedListener(
+            @NonNull Executor executor, @NonNull Runnable listener) {
+        DeviceConfig.OnPropertiesChangedListener onChangedListener =
+                properties -> {
+                    if (properties.getKeyset().contains(
+                            getFeatureName())) {
+                        listener.run();
+                    }
+                };
+
+        clearCallingIdentifyAndTryCatch(
+                () -> DeviceConfig.addOnPropertiesChangedListener(
+                        getNamespace(),
+                        executor,
+                        onChangedListener),
+                () -> {});
+
+        return onChangedListener;
+    }
+
+    /**
+     * Remove a listener for when the feature flag changes.
+     *
+     * <p>{@see DeviceConfig#addOnPropertiesChangedListener(String, Executor,
+     * DeviceConfig.OnPropertiesChangedListener)}
+     */
+    public void removeOnChangedListener(
+            @NonNull DeviceConfig.OnPropertiesChangedListener onChangedListener) {
+        DeviceConfig.removeOnPropertiesChangedListener(onChangedListener);
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
index 5a783f4..03fa93d 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnail.java
@@ -58,7 +58,9 @@
     @VisibleForTesting
     public final FrameLayout mThumbnailLayout;
 
-    private final View mThumbNailView;
+    private final View mThumbnailView;
+    private int mThumbnailWidth;
+    private int mThumbnailHeight;
 
     private final WindowManager.LayoutParams mBackgroundParams;
     private boolean mVisible = false;
@@ -66,7 +68,7 @@
     private static final float ASPECT_RATIO = 14f;
     private static final float BG_ASPECT_RATIO = ASPECT_RATIO / 2f;
 
-    private ObjectAnimator mThumbNailAnimator;
+    private ObjectAnimator mThumbnailAnimator;
     private boolean mIsFadingIn;
 
     /**
@@ -79,9 +81,11 @@
         mWindowBounds =  mWindowManager.getCurrentWindowMetrics().getBounds();
         mThumbnailLayout = (FrameLayout) LayoutInflater.from(mContext)
                 .inflate(R.layout.thumbnail_background_view, /* root: */ null);
-        mThumbNailView =
+        mThumbnailView =
                 mThumbnailLayout.findViewById(R.id.accessibility_magnification_thumbnail_view);
         mBackgroundParams = createLayoutParams();
+        mThumbnailWidth = 0;
+        mThumbnailHeight = 0;
     }
 
     /**
@@ -90,35 +94,35 @@
      * @param currentBounds the current magnification bounds
      */
     @AnyThread
-    public void setThumbNailBounds(Rect currentBounds, float scale, float centerX, float centerY) {
+    public void setThumbnailBounds(Rect currentBounds, float scale, float centerX, float centerY) {
         if (DEBUG) {
-            Log.d(LOG_TAG, "setThumbNailBounds " + currentBounds);
+            Log.d(LOG_TAG, "setThumbnailBounds " + currentBounds);
         }
         mHandler.post(() -> {
             mWindowBounds = currentBounds;
             setBackgroundBounds();
             if (mVisible) {
-                updateThumbNailMainThread(scale, centerX, centerY);
+                updateThumbnailMainThread(scale, centerX, centerY);
             }
         });
     }
 
     private void setBackgroundBounds() {
         Point magnificationBoundary = getMagnificationThumbnailPadding(mContext);
-        final int thumbNailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO);
-        final int thumbNailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO);
+        mThumbnailWidth = (int) (mWindowBounds.width() / BG_ASPECT_RATIO);
+        mThumbnailHeight = (int) (mWindowBounds.height() / BG_ASPECT_RATIO);
         int initX = magnificationBoundary.x;
         int initY = magnificationBoundary.y;
-        mBackgroundParams.width = thumbNailWidth;
-        mBackgroundParams.height = thumbNailHeight;
+        mBackgroundParams.width = mThumbnailWidth;
+        mBackgroundParams.height = mThumbnailHeight;
         mBackgroundParams.x = initX;
         mBackgroundParams.y = initY;
     }
 
     @MainThread
-    private void showThumbNail() {
+    private void showThumbnail() {
         if (DEBUG) {
-            Log.d(LOG_TAG, "showThumbNail " + mVisible);
+            Log.d(LOG_TAG, "showThumbnail " + mVisible);
         }
         animateThumbnail(true);
     }
@@ -127,14 +131,14 @@
      * Hides thumbnail and removes the view from the window when finished animating.
      */
     @AnyThread
-    public void hideThumbNail() {
-        mHandler.post(this::hideThumbNailMainThread);
+    public void hideThumbnail() {
+        mHandler.post(this::hideThumbnailMainThread);
     }
 
     @MainThread
-    private void hideThumbNailMainThread() {
+    private void hideThumbnailMainThread() {
         if (DEBUG) {
-            Log.d(LOG_TAG, "hideThumbNail " + mVisible);
+            Log.d(LOG_TAG, "hideThumbnail " + mVisible);
         }
         if (mVisible) {
             animateThumbnail(false);
@@ -155,14 +159,14 @@
                         + " fadeIn: " + fadeIn
                         + " mVisible: " + mVisible
                         + " isFadingIn: " + mIsFadingIn
-                        + " isRunning: " + mThumbNailAnimator
+                        + " isRunning: " + mThumbnailAnimator
             );
         }
 
         // Reset countdown to hide automatically
-        mHandler.removeCallbacks(this::hideThumbNailMainThread);
+        mHandler.removeCallbacks(this::hideThumbnailMainThread);
         if (fadeIn) {
-            mHandler.postDelayed(this::hideThumbNailMainThread, LINGER_DURATION_MS);
+            mHandler.postDelayed(this::hideThumbnailMainThread, LINGER_DURATION_MS);
         }
 
         if (fadeIn == mIsFadingIn) {
@@ -175,18 +179,18 @@
             mVisible = true;
         }
 
-        if (mThumbNailAnimator != null) {
-            mThumbNailAnimator.cancel();
+        if (mThumbnailAnimator != null) {
+            mThumbnailAnimator.cancel();
         }
-        mThumbNailAnimator = ObjectAnimator.ofFloat(
+        mThumbnailAnimator = ObjectAnimator.ofFloat(
                 mThumbnailLayout,
                 "alpha",
                 fadeIn ? 1f : 0f
         );
-        mThumbNailAnimator.setDuration(
+        mThumbnailAnimator.setDuration(
                 fadeIn ? FADE_IN_ANIMATION_DURATION_MS : FADE_OUT_ANIMATION_DURATION_MS
         );
-        mThumbNailAnimator.addListener(new Animator.AnimatorListener() {
+        mThumbnailAnimator.addListener(new Animator.AnimatorListener() {
             private boolean mIsCancelled;
 
             @Override
@@ -231,7 +235,7 @@
             }
         });
 
-        mThumbNailAnimator.start();
+        mThumbnailAnimator.start();
     }
 
     /**
@@ -246,38 +250,48 @@
      *                of the viewport, or {@link Float#NaN} to leave unchanged
      */
     @AnyThread
-    public void updateThumbNail(float scale, float centerX, float centerY) {
-        mHandler.post(() -> updateThumbNailMainThread(scale, centerX, centerY));
+    public void updateThumbnail(float scale, float centerX, float centerY) {
+        mHandler.post(() -> updateThumbnailMainThread(scale, centerX, centerY));
     }
 
     @MainThread
-    private void updateThumbNailMainThread(float scale, float centerX, float centerY) {
+    private void updateThumbnailMainThread(float scale, float centerX, float centerY) {
         // Restart the fadeout countdown (or show if it's hidden)
-        showThumbNail();
+        showThumbnail();
 
-        var scaleDown = Float.isNaN(scale) ? mThumbNailView.getScaleX() : 1f / scale;
+        var scaleDown = Float.isNaN(scale) ? mThumbnailView.getScaleX() : 1f / scale;
         if (!Float.isNaN(scale)) {
-            mThumbNailView.setScaleX(scaleDown);
-            mThumbNailView.setScaleY(scaleDown);
+            mThumbnailView.setScaleX(scaleDown);
+            mThumbnailView.setScaleY(scaleDown);
+        }
+        float thumbnailWidth;
+        float thumbnailHeight;
+        if (mThumbnailView.getWidth() == 0 || mThumbnailView.getHeight() == 0) {
+            // if the thumbnail view size is not updated correctly, we just use the cached values.
+            thumbnailWidth = mThumbnailWidth;
+            thumbnailHeight = mThumbnailHeight;
+        } else {
+            thumbnailWidth = mThumbnailView.getWidth();
+            thumbnailHeight = mThumbnailView.getHeight();
         }
         if (!Float.isNaN(centerX)) {
-            var padding = mThumbNailView.getPaddingTop();
+            var padding = mThumbnailView.getPaddingTop();
             var ratio = 1f / BG_ASPECT_RATIO;
-            var centerXScaled = centerX * ratio - (mThumbNailView.getWidth() / 2f + padding);
-            var centerYScaled = centerY * ratio - (mThumbNailView.getHeight() / 2f + padding);
+            var centerXScaled = centerX * ratio - (thumbnailWidth / 2f + padding);
+            var centerYScaled = centerY * ratio - (thumbnailHeight / 2f + padding);
 
             if (DEBUG) {
                 Log.d(
                         LOG_TAG,
-                        "updateThumbNail centerXScaled : " + centerXScaled
+                        "updateThumbnail centerXScaled : " + centerXScaled
                                 + " centerYScaled : " + centerYScaled
-                                + " getTranslationX : " + mThumbNailView.getTranslationX()
+                                + " getTranslationX : " + mThumbnailView.getTranslationX()
                                 + " ratio : " + ratio
                 );
             }
 
-            mThumbNailView.setTranslationX(centerXScaled);
-            mThumbNailView.setTranslationY(centerYScaled);
+            mThumbnailView.setTranslationX(centerXScaled);
+            mThumbnailView.setTranslationY(centerYScaled);
         }
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnailFeatureFlag.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnailFeatureFlag.java
new file mode 100644
index 0000000..519f31b
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationThumbnailFeatureFlag.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.magnification;
+
+import android.provider.DeviceConfig;
+
+/**
+ * Encapsulates the feature flags for magnification thumbnail. {@see DeviceConfig}
+ *
+ * @hide
+ */
+public class MagnificationThumbnailFeatureFlag extends MagnificationFeatureFlagBase {
+
+    private static final String NAMESPACE = DeviceConfig.NAMESPACE_ACCESSIBILITY;
+    private static final String FEATURE_NAME_ENABLE_MAGNIFIER_THUMBNAIL =
+            "enable_magnifier_thumbnail";
+
+    @Override
+    String getNamespace() {
+        return NAMESPACE;
+    }
+
+    @Override
+    String getFeatureName() {
+        return FEATURE_NAME_ENABLE_MAGNIFIER_THUMBNAIL;
+    }
+
+    @Override
+    boolean getDefaultValue() {
+        return false;
+    }
+}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3bd4547..b2e8ffc 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -4277,7 +4277,7 @@
 
         getUiForShowing().showFillUi(filledId, response, filterText,
                 mService.getServicePackageName(), mComponentName,
-                targetLabel, targetIcon, this, id, mCompatMode);
+                targetLabel, targetIcon, this, userId, id, mCompatMode);
 
         synchronized (mLock) {
             mPresentationStatsEventLogger.maybeSetCountShown(
diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
index 8291610..a631818 100644
--- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
@@ -23,6 +23,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -196,17 +197,19 @@
      * @param serviceLabel label of autofill service
      * @param serviceIcon icon of autofill service
      * @param callback identifier for the caller
+     * @param userId the user associated wit the session
      * @param sessionId id of the autofill session
      * @param compatMode whether the app is being autofilled in compatibility mode.
      */
     public void showFillUi(@NonNull AutofillId focusedId, @NonNull FillResponse response,
             @Nullable String filterText, @Nullable String servicePackageName,
             @NonNull ComponentName componentName, @NonNull CharSequence serviceLabel,
-            @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback, int sessionId,
-            boolean compatMode) {
+            @NonNull Drawable serviceIcon, @NonNull AutoFillUiCallback callback,
+            @UserIdInt int userId, int sessionId, boolean compatMode) {
         if (sDebug) {
             final int size = filterText == null ? 0 : filterText.length();
-            Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars");
+            Slog.d(TAG, "showFillUi(): id=" + focusedId + ", filter=" + size + " chars, userId="
+                    + userId);
         }
         final LogMaker log = Helper
                 .newLogMaker(MetricsEvent.AUTOFILL_FILL_UI, componentName, servicePackageName,
@@ -221,7 +224,7 @@
                 return;
             }
             hideAllUiThread(callback);
-            mFillUi = new FillUi(mContext, response, focusedId,
+            mFillUi = new FillUi(mContext, userId, response, focusedId,
                     filterText, mOverlayControl, serviceLabel, serviceIcon,
                     mUiModeMgr.isNightMode(),
                     new FillUi.Callback() {
diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
index 76f4505..30d2fe4 100644
--- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java
@@ -22,12 +22,15 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.content.Context;
 import android.content.IntentSender;
 import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.os.UserManager;
 import android.service.autofill.Dataset;
 import android.service.autofill.Dataset.DatasetFieldFilter;
 import android.service.autofill.FillResponse;
@@ -36,6 +39,7 @@
 import android.util.Slog;
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
+import android.view.Display;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -57,9 +61,12 @@
 import android.widget.TextView;
 
 import com.android.internal.R;
+import com.android.server.LocalServices;
 import com.android.server.UiThread;
 import com.android.server.autofill.AutofillManagerService;
 import com.android.server.autofill.Helper;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -133,13 +140,29 @@
         return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
     }
 
-    FillUi(@NonNull Context context, @NonNull FillResponse response,
+    FillUi(@NonNull Context context, @UserIdInt int userId, @NonNull FillResponse response,
             @NonNull AutofillId focusedViewId, @Nullable String filterText,
             @NonNull OverlayControl overlayControl, @NonNull CharSequence serviceLabel,
             @NonNull Drawable serviceIcon, boolean nightMode, @NonNull Callback callback) {
         if (sVerbose) Slog.v(TAG, "nightMode: " + nightMode);
         mThemeId = nightMode ? THEME_ID_DARK : THEME_ID_LIGHT;
         mCallback = callback;
+
+        if (UserManager.isVisibleBackgroundUsersEnabled()) {
+            UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+            int displayId = umi.getMainDisplayAssignedToUser(userId);
+            if (sDebug) {
+                Slogf.d(TAG, "Creating context for display %d for user %d", displayId, userId);
+            }
+            Display display = context.getSystemService(DisplayManager.class).getDisplay(displayId);
+            if (display != null) {
+                context = context.createDisplayContext(display);
+            } else {
+                Slogf.d(TAG, "Could not get display with id %d (which is associated with user %d; "
+                        + "FillUi operations will probably fail", displayId, userId);
+            }
+        }
+
         mFullScreen = isFullScreen(context);
         mContext = new ContextThemeWrapper(context, mThemeId);
 
@@ -774,6 +797,7 @@
         pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth);
         pw.print(prefix); pw.print("mContentHeight: "); pw.println(mContentHeight);
         pw.print(prefix); pw.print("mDestroyed: "); pw.println(mDestroyed);
+        pw.print(prefix); pw.print("mContext: "); pw.println(mContext);
         pw.print(prefix); pw.print("theme id: "); pw.print(mThemeId);
         switch (mThemeId) {
             case THEME_ID_DARK:
diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java
new file mode 100644
index 0000000..19d8b87
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.datatransfer.contextsync;
+
+import android.content.ComponentName;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/** Service for Telecom to bind to when call metadata is synced between devices. */
+public class CallMetadataSyncConnectionService extends ConnectionService {
+
+    private TelecomManager mTelecomManager;
+    private final Map<String, PhoneAccountHandle> mPhoneAccountHandles = new HashMap<>();
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mTelecomManager = getSystemService(TelecomManager.class);
+    }
+
+    /**
+     * Registers a {@link android.telecom.PhoneAccount} for a given call-capable app on the synced
+     * device.
+     */
+    public void registerPhoneAccount(String packageName, String humanReadableAppName) {
+        final PhoneAccount phoneAccount = createPhoneAccount(packageName, humanReadableAppName);
+        if (phoneAccount != null) {
+            mTelecomManager.registerPhoneAccount(phoneAccount);
+            mTelecomManager.enablePhoneAccount(mPhoneAccountHandles.get(packageName), true);
+        }
+    }
+
+    /**
+     * Unregisters a {@link android.telecom.PhoneAccount} for a given call-capable app on the synced
+     * device.
+     */
+    public void unregisterPhoneAccount(String packageName) {
+        mTelecomManager.unregisterPhoneAccount(mPhoneAccountHandles.remove(packageName));
+    }
+
+    @VisibleForTesting
+    PhoneAccount createPhoneAccount(String packageName, String humanReadableAppName) {
+        if (mPhoneAccountHandles.containsKey(packageName)) {
+            // Already exists!
+            return null;
+        }
+        final PhoneAccountHandle handle = new PhoneAccountHandle(
+                new ComponentName(this, CallMetadataSyncConnectionService.class),
+                UUID.randomUUID().toString());
+        mPhoneAccountHandles.put(packageName, handle);
+        return new PhoneAccount.Builder(handle, humanReadableAppName)
+                .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
+                        | PhoneAccount.CAPABILITY_SELF_MANAGED).build();
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java
new file mode 100644
index 0000000..1e4bb9a
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncData.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.datatransfer.contextsync;
+
+import android.annotation.NonNull;
+import android.companion.ContextSyncMessage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** A read-only snapshot of an {@link ContextSyncMessage}. */
+class CallMetadataSyncData {
+
+    final Map<Long, CallMetadataSyncData.Call> mCalls = new HashMap<>();
+    final List<CallMetadataSyncData.Call> mRequests = new ArrayList<>();
+
+    public void addCall(CallMetadataSyncData.Call call) {
+        mCalls.put(call.getId(), call);
+    }
+
+    public boolean hasCall(long id) {
+        return mCalls.containsKey(id);
+    }
+
+    public Collection<CallMetadataSyncData.Call> getCalls() {
+        return mCalls.values();
+    }
+
+    public void addRequest(CallMetadataSyncData.Call call) {
+        mRequests.add(call);
+    }
+
+    public List<CallMetadataSyncData.Call> getRequests() {
+        return mRequests;
+    }
+
+    public static class Call implements Parcelable {
+        private long mId;
+        private String mCallerId;
+        private byte[] mAppIcon;
+        private String mAppName;
+        private String mAppIdentifier;
+        private int mStatus;
+        private final Set<Integer> mControls = new HashSet<>();
+
+        public static Call fromParcel(Parcel parcel) {
+            final Call call = new Call();
+            call.setId(parcel.readLong());
+            call.setCallerId(parcel.readString());
+            call.setAppIcon(parcel.readBlob());
+            call.setAppName(parcel.readString());
+            call.setAppIdentifier(parcel.readString());
+            call.setStatus(parcel.readInt());
+            final int numberOfControls = parcel.readInt();
+            for (int i = 0; i < numberOfControls; i++) {
+                call.addControl(parcel.readInt());
+            }
+            return call;
+        }
+
+        @Override
+        public void writeToParcel(Parcel parcel, int parcelableFlags) {
+            parcel.writeLong(mId);
+            parcel.writeString(mCallerId);
+            parcel.writeBlob(mAppIcon);
+            parcel.writeString(mAppName);
+            parcel.writeString(mAppIdentifier);
+            parcel.writeInt(mStatus);
+            parcel.writeInt(mControls.size());
+            for (int control : mControls) {
+                parcel.writeInt(control);
+            }
+        }
+
+        void setId(long id) {
+            mId = id;
+        }
+
+        void setCallerId(String callerId) {
+            mCallerId = callerId;
+        }
+
+        void setAppIcon(byte[] appIcon) {
+            mAppIcon = appIcon;
+        }
+
+        void setAppName(String appName) {
+            mAppName = appName;
+        }
+
+        void setAppIdentifier(String appIdentifier) {
+            mAppIdentifier = appIdentifier;
+        }
+
+        void setStatus(int status) {
+            mStatus = status;
+        }
+
+        void addControl(int control) {
+            mControls.add(control);
+        }
+
+        long getId() {
+            return mId;
+        }
+
+        String getCallerId() {
+            return mCallerId;
+        }
+
+        byte[] getAppIcon() {
+            return mAppIcon;
+        }
+
+        String getAppName() {
+            return mAppName;
+        }
+
+        String getAppIdentifier() {
+            return mAppIdentifier;
+        }
+
+        int getStatus() {
+            return mStatus;
+        }
+
+        Set<Integer> getControls() {
+            return mControls;
+        }
+
+        boolean hasControl(int control) {
+            return mControls.contains(control);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof CallMetadataSyncData.Call) {
+                return ((Call) other).getId() == getId();
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(mId);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @NonNull public static final Parcelable.Creator<Call> CREATOR = new Parcelable.Creator<>() {
+
+            @Override
+            public Call createFromParcel(Parcel source) {
+                return Call.fromParcel(source);
+            }
+
+            @Override
+            public Call[] newArray(int size) {
+                return new Call[size];
+            }
+        };
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java
index 077fd2a..dd0bbf2 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CrossDeviceCall.java
@@ -39,6 +39,8 @@
 
     private static final String TAG = "CrossDeviceCall";
 
+    public static final String EXTRA_CALL_ID =
+            "com.android.companion.datatransfer.contextsync.extra.CALL_ID";
     private static final int APP_ICON_BITMAP_DIMENSION = 256;
 
     private static final AtomicLong sNextId = new AtomicLong(1);
@@ -47,6 +49,7 @@
     private final Call mCall;
     @VisibleForTesting boolean mIsEnterprise;
     @VisibleForTesting boolean mIsOtt;
+    private final String mCallingAppPackageName;
     private String mCallingAppName;
     private byte[] mCallingAppIcon;
     private String mCallerDisplayName;
@@ -59,7 +62,7 @@
             CallAudioState callAudioState) {
         mId = sNextId.getAndIncrement();
         mCall = call;
-        final String callingAppPackageName = call != null
+        mCallingAppPackageName = call != null
                 ? call.getDetails().getAccountHandle().getComponentName().getPackageName() : null;
         mIsOtt = call != null
                 && (call.getDetails().getCallCapabilities() & Call.Details.PROPERTY_SELF_MANAGED)
@@ -69,13 +72,13 @@
                 == Call.Details.PROPERTY_ENTERPRISE_CALL;
         try {
             final ApplicationInfo applicationInfo = packageManager
-                    .getApplicationInfo(callingAppPackageName,
+                    .getApplicationInfo(mCallingAppPackageName,
                             PackageManager.ApplicationInfoFlags.of(0));
             mCallingAppName = packageManager.getApplicationLabel(applicationInfo).toString();
             mCallingAppIcon = renderDrawableToByteArray(
                     packageManager.getApplicationIcon(applicationInfo));
         } catch (PackageManager.NameNotFoundException e) {
-            Slog.e(TAG, "Could not get application info for package " + callingAppPackageName, e);
+            Slog.e(TAG, "Could not get application info for package " + mCallingAppPackageName, e);
         }
         mIsMuted = callAudioState != null && callAudioState.isMuted();
         if (call != null) {
@@ -170,7 +173,8 @@
         }
     }
 
-    private int convertStateToStatus(int callState) {
+    /** Converts a Telecom call state to a Context Sync status. */
+    public static int convertStateToStatus(int callState) {
         switch (callState) {
             case Call.STATE_HOLDING:
                 return android.companion.Telecom.Call.ON_HOLD;
@@ -178,20 +182,30 @@
                 return android.companion.Telecom.Call.ONGOING;
             case Call.STATE_RINGING:
                 return android.companion.Telecom.Call.RINGING;
-            case Call.STATE_NEW:
-            case Call.STATE_DIALING:
-            case Call.STATE_DISCONNECTED:
-            case Call.STATE_SELECT_PHONE_ACCOUNT:
-            case Call.STATE_CONNECTING:
-            case Call.STATE_DISCONNECTING:
-            case Call.STATE_PULLING_CALL:
-            case Call.STATE_AUDIO_PROCESSING:
-            case Call.STATE_SIMULATED_RINGING:
             default:
                 return android.companion.Telecom.Call.UNKNOWN_STATUS;
         }
     }
 
+    /**
+     * Converts a Context Sync status to a Telecom call state. Note that this is lossy for
+     * and RINGING_SILENCED, as Telecom does not distinguish between RINGING and RINGING_SILENCED.
+     */
+    public static int convertStatusToState(int status) {
+        switch (status) {
+            case android.companion.Telecom.Call.ON_HOLD:
+                return Call.STATE_HOLDING;
+            case android.companion.Telecom.Call.ONGOING:
+                return Call.STATE_ACTIVE;
+            case android.companion.Telecom.Call.RINGING:
+            case android.companion.Telecom.Call.RINGING_SILENCED:
+                return Call.STATE_RINGING;
+            case android.companion.Telecom.Call.UNKNOWN_STATUS:
+            default:
+                return Call.STATE_NEW;
+        }
+    }
+
     public long getId() {
         return mId;
     }
@@ -208,6 +222,10 @@
         return mCallingAppIcon;
     }
 
+    public String getCallingAppPackageName() {
+        return mCallingAppPackageName;
+    }
+
     /**
      * Get a human-readable "caller id" to display as the origin of the call.
      *
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 1a0588e..307f7bf 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -367,7 +367,7 @@
                         "Could not send key event to input device for given token");
             }
             return mNativeWrapper.writeDpadKeyEvent(inputDeviceDescriptor.getNativePointer(),
-                    event.getKeyCode(), event.getAction());
+                    event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
         }
     }
 
@@ -380,7 +380,7 @@
                         "Could not send key event to input device for given token");
             }
             return mNativeWrapper.writeKeyEvent(inputDeviceDescriptor.getNativePointer(),
-                    event.getKeyCode(), event.getAction());
+                    event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
         }
     }
 
@@ -398,7 +398,7 @@
                         "Display id associated with this mouse is not currently targetable");
             }
             return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
-                    event.getButtonCode(), event.getAction());
+                    event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
         }
     }
 
@@ -412,7 +412,8 @@
             }
             return mNativeWrapper.writeTouchEvent(inputDeviceDescriptor.getNativePointer(),
                     event.getPointerId(), event.getToolType(), event.getAction(), event.getX(),
-                    event.getY(), event.getPressure(), event.getMajorAxisSize());
+                    event.getY(), event.getPressure(), event.getMajorAxisSize(),
+                    event.getEventTimeNanos());
         }
     }
 
@@ -430,7 +431,7 @@
                         "Display id associated with this mouse is not currently targetable");
             }
             return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
-                    event.getRelativeX(), event.getRelativeY());
+                    event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
         }
     }
 
@@ -448,7 +449,7 @@
                         "Display id associated with this mouse is not currently targetable");
             }
             return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
-                    event.getXAxisMovement(), event.getYAxisMovement());
+                    event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
         }
     }
 
@@ -514,15 +515,19 @@
     private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId,
             int productId, String phys, int height, int width);
     private static native void nativeCloseUinput(long ptr);
-    private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action);
-    private static native boolean nativeWriteKeyEvent(long ptr, int androidKeyCode, int action);
-    private static native boolean nativeWriteButtonEvent(long ptr, int buttonCode, int action);
+    private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action,
+            long eventTimeNanos);
+    private static native boolean nativeWriteKeyEvent(long ptr, int androidKeyCode, int action,
+            long eventTimeNanos);
+    private static native boolean nativeWriteButtonEvent(long ptr, int buttonCode, int action,
+            long eventTimeNanos);
     private static native boolean nativeWriteTouchEvent(long ptr, int pointerId, int toolType,
-            int action, float locationX, float locationY, float pressure, float majorAxisSize);
+            int action, float locationX, float locationY, float pressure, float majorAxisSize,
+            long eventTimeNanos);
     private static native boolean nativeWriteRelativeEvent(long ptr, float relativeX,
-            float relativeY);
+            float relativeY, long eventTimeNanos);
     private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement,
-            float yAxisMovement);
+            float yAxisMovement, long eventTimeNanos);
 
     /** Wrapper around the static native methods for tests. */
     @VisibleForTesting
@@ -550,32 +555,37 @@
             nativeCloseUinput(ptr);
         }
 
-        public boolean writeDpadKeyEvent(long ptr, int androidKeyCode, int action) {
-            return nativeWriteDpadKeyEvent(ptr, androidKeyCode, action);
+        public boolean writeDpadKeyEvent(long ptr, int androidKeyCode, int action,
+                long eventTimeNanos) {
+            return nativeWriteDpadKeyEvent(ptr, androidKeyCode, action, eventTimeNanos);
         }
 
-        public boolean writeKeyEvent(long ptr, int androidKeyCode, int action) {
-            return nativeWriteKeyEvent(ptr, androidKeyCode, action);
+        public boolean writeKeyEvent(long ptr, int androidKeyCode, int action,
+                long eventTimeNanos) {
+            return nativeWriteKeyEvent(ptr, androidKeyCode, action, eventTimeNanos);
         }
 
-        public boolean writeButtonEvent(long ptr, int buttonCode, int action) {
-            return nativeWriteButtonEvent(ptr, buttonCode, action);
+        public boolean writeButtonEvent(long ptr, int buttonCode, int action,
+                long eventTimeNanos) {
+            return nativeWriteButtonEvent(ptr, buttonCode, action, eventTimeNanos);
         }
 
         public boolean writeTouchEvent(long ptr, int pointerId, int toolType, int action,
-                float locationX, float locationY, float pressure, float majorAxisSize) {
+                float locationX, float locationY, float pressure, float majorAxisSize,
+                long eventTimeNanos) {
             return nativeWriteTouchEvent(ptr, pointerId, toolType,
                     action, locationX, locationY,
-                    pressure, majorAxisSize);
+                    pressure, majorAxisSize, eventTimeNanos);
         }
 
-        public boolean writeRelativeEvent(long ptr, float relativeX, float relativeY) {
-            return nativeWriteRelativeEvent(ptr, relativeX, relativeY);
+        public boolean writeRelativeEvent(long ptr, float relativeX, float relativeY,
+                long eventTimeNanos) {
+            return nativeWriteRelativeEvent(ptr, relativeX, relativeY, eventTimeNanos);
         }
 
-        public boolean writeScrollEvent(long ptr, float xAxisMovement, float yAxisMovement) {
-            return nativeWriteScrollEvent(ptr, xAxisMovement,
-                    yAxisMovement);
+        public boolean writeScrollEvent(long ptr, float xAxisMovement, float yAxisMovement,
+                long eventTimeNanos) {
+            return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos);
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index ae88f24..de0f68c 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -404,39 +404,44 @@
     public void close() {
         super.close_enforcePermission();
         // Remove about-to-be-closed virtual device from the service before butchering it.
-        mService.removeVirtualDevice(mDeviceId);
+        boolean removed = mService.removeVirtualDevice(mDeviceId);
         mDeviceId = Context.DEVICE_ID_INVALID;
 
-        VirtualDisplayWrapper[] virtualDisplaysToBeReleased;
-        synchronized (mVirtualDeviceLock) {
-            if (mVirtualAudioController != null) {
-                mVirtualAudioController.stopListening();
-                mVirtualAudioController = null;
-            }
-            mLocaleList = null;
-            virtualDisplaysToBeReleased = new VirtualDisplayWrapper[mVirtualDisplays.size()];
-            for (int i = 0; i < mVirtualDisplays.size(); i++) {
-                virtualDisplaysToBeReleased[i] = mVirtualDisplays.valueAt(i);
-            }
-            mVirtualDisplays.clear();
-            mVirtualSensorList = null;
-            mVirtualSensors.clear();
+        // Device is already closed.
+        if (!removed) {
+            return;
         }
-        // Destroy the display outside locked section.
-        for (VirtualDisplayWrapper virtualDisplayWrapper : virtualDisplaysToBeReleased) {
-            mDisplayManager.releaseVirtualDisplay(virtualDisplayWrapper.getToken());
-            // The releaseVirtualDisplay call above won't trigger
-            // VirtualDeviceImpl.onVirtualDisplayRemoved callback because we already removed the
-            // virtual device from the service - we release the other display-tied resources here
-            // with the guarantee it will be done exactly once.
-            releaseOwnedVirtualDisplayResources(virtualDisplayWrapper);
-        }
-
-        mAppToken.unlinkToDeath(this, 0);
-        mCameraAccessController.stopObservingIfNeeded();
 
         final long ident = Binder.clearCallingIdentity();
         try {
+            VirtualDisplayWrapper[] virtualDisplaysToBeReleased;
+            synchronized (mVirtualDeviceLock) {
+                if (mVirtualAudioController != null) {
+                    mVirtualAudioController.stopListening();
+                    mVirtualAudioController = null;
+                }
+                mLocaleList = null;
+                virtualDisplaysToBeReleased = new VirtualDisplayWrapper[mVirtualDisplays.size()];
+                for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                    virtualDisplaysToBeReleased[i] = mVirtualDisplays.valueAt(i);
+                }
+                mVirtualDisplays.clear();
+                mVirtualSensorList = null;
+                mVirtualSensors.clear();
+            }
+            // Destroy the display outside locked section.
+            for (VirtualDisplayWrapper virtualDisplayWrapper : virtualDisplaysToBeReleased) {
+                mDisplayManager.releaseVirtualDisplay(virtualDisplayWrapper.getToken());
+                // The releaseVirtualDisplay call above won't trigger
+                // VirtualDeviceImpl.onVirtualDisplayRemoved callback because we already removed the
+                // virtual device from the service - we release the other display-tied resources
+                // here with the guarantee it will be done exactly once.
+                releaseOwnedVirtualDisplayResources(virtualDisplayWrapper);
+            }
+
+            mAppToken.unlinkToDeath(this, 0);
+            mCameraAccessController.stopObservingIfNeeded();
+
             mInputController.close();
             mSensorController.close();
         } finally {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 9644642..ad4c0bf 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -202,8 +202,19 @@
         }
     }
 
-    void removeVirtualDevice(int deviceId) {
+    /**
+     * Remove the virtual device. Sends the
+     * {@link VirtualDeviceManager#ACTION_VIRTUAL_DEVICE_REMOVED} broadcast as a result.
+     *
+     * @param deviceId deviceId to be removed
+     * @return {@code true} if the device was removed, {@code false} if the operation was a no-op
+     */
+    boolean removeVirtualDevice(int deviceId) {
         synchronized (mVirtualDeviceManagerLock) {
+            if (!mVirtualDevices.contains(deviceId)) {
+                return false;
+            }
+
             mAppsOnVirtualDevices.remove(deviceId);
             mVirtualDevices.remove(deviceId);
         }
@@ -223,6 +234,7 @@
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
+        return true;
     }
 
     private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) {
@@ -248,7 +260,6 @@
         for (VirtualDeviceImpl virtualDevice : virtualDevicesToRemove) {
             virtualDevice.close();
         }
-
     }
 
     private void registerCdmAssociationListener() {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index cfdf3ac..f8d19ec 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -161,6 +161,7 @@
         "android.hardware.health-V2-java", // AIDL
         "android.hardware.health-translate-java",
         "android.hardware.light-V1-java",
+        "android.hardware.security.rkp-V3-java",
         "android.hardware.tv.cec-V1.1-java",
         "android.hardware.tv.hdmi.cec-V1-java",
         "android.hardware.tv.hdmi.connection-V1-java",
@@ -177,6 +178,7 @@
         "android.hardware.power.stats-V2-java",
         "android.hardware.power-V4-java",
         "android.hidl.manager-V1.2-java",
+        "cbor-java",
         "icu4j_calendar_astronomer",
         "netd-client",
         "overlayable_policy_aidl-java",
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 12ee131..0713999 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -17,7 +17,6 @@
 package android.os;
 
 import android.annotation.IntDef;
-import android.annotation.NonNull;
 import android.net.Network;
 
 import com.android.internal.os.BinderCallsStats;
@@ -40,6 +39,8 @@
     public static final int CPU_WAKEUP_SUBSYSTEM_ALARM = 1;
     public static final int CPU_WAKEUP_SUBSYSTEM_WIFI = 2;
     public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3;
+    public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4;
+    public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5;
 
     /** @hide */
     @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
@@ -47,9 +48,11 @@
             CPU_WAKEUP_SUBSYSTEM_ALARM,
             CPU_WAKEUP_SUBSYSTEM_WIFI,
             CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
+            CPU_WAKEUP_SUBSYSTEM_SENSOR,
+            CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
     })
     @Retention(RetentionPolicy.SOURCE)
-    @interface CpuWakeupSubsystem {
+    public @interface CpuWakeupSubsystem {
     }
 
     /**
@@ -107,19 +110,16 @@
     public abstract void noteBinderThreadNativeIds(int[] binderThreadNativeTids);
 
     /**
-     * Reports any activity that could potentially have caused the CPU to wake up.
-     * Accepts a timestamp to allow free ordering between the event and its reporting.
-     * @param subsystem The subsystem this activity should be attributed to.
-     * @param elapsedMillis The time when this activity happened in the elapsed timebase.
-     * @param uids The uid (or uids) that should be blamed for this activity.
-     */
-    public abstract void noteCpuWakingActivity(@CpuWakeupSubsystem int subsystem,
-            long elapsedMillis, @NonNull int... uids);
-
-    /**
      * Reports a sound trigger recognition event that may have woken up the CPU.
      * @param elapsedMillis The time when the event happened in the elapsed timebase.
      * @param uid The uid that requested this trigger.
      */
     public abstract void noteWakingSoundTrigger(long elapsedMillis, int uid);
+
+    /**
+     * Reports an alarm batch that would have woken up the CPU.
+     * @param elapsedMillis The time at which this alarm batch was scheduled to go off.
+     * @param uids the uids of all apps that have any alarm in this batch.
+     */
+    public abstract void noteWakingAlarmBatch(long elapsedMillis, int... uids);
 }
diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java
index d256aea..f4f5c95 100644
--- a/services/core/java/com/android/server/PackageWatchdog.java
+++ b/services/core/java/com/android/server/PackageWatchdog.java
@@ -580,6 +580,7 @@
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
+                     PackageHealthObserverImpact.USER_IMPACT_LEVEL_60,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
                      PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
     public @interface PackageHealthObserverImpact {
@@ -590,6 +591,7 @@
         /* Actions having medium user impact, user of a device will likely notice. */
         int USER_IMPACT_LEVEL_30 = 30;
         int USER_IMPACT_LEVEL_50 = 50;
+        int USER_IMPACT_LEVEL_60 = 60;
         int USER_IMPACT_LEVEL_70 = 70;
         /* Action has high user impact, a last resort, user of a device will be very frustrated. */
         int USER_IMPACT_LEVEL_100 = 100;
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index c0b3a90..d140403 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -23,6 +23,7 @@
 import static android.Manifest.permission.POWER_SAVER;
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
 import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE;
@@ -51,6 +52,7 @@
 import android.os.BatteryManagerInternal;
 import android.os.BatteryStats;
 import android.os.BatteryStatsInternal;
+import android.os.BatteryStatsInternal.CpuWakeupSubsystem;
 import android.os.BatteryUsageStats;
 import android.os.BatteryUsageStatsQuery;
 import android.os.Binder;
@@ -474,6 +476,8 @@
         private int transportToSubsystem(NetworkCapabilities nc) {
             if (nc.hasTransport(TRANSPORT_WIFI)) {
                 return CPU_WAKEUP_SUBSYSTEM_WIFI;
+            } else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
+                return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
             }
             return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
         }
@@ -514,14 +518,32 @@
         }
 
         @Override
-        public void noteCpuWakingActivity(int subsystem, long elapsedMillis, int... uids) {
-            Objects.requireNonNull(uids);
-            mHandler.post(() -> mCpuWakeupStats.noteWakingActivity(subsystem, elapsedMillis, uids));
-        }
-        @Override
         public void noteWakingSoundTrigger(long elapsedMillis, int uid) {
             noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, elapsedMillis, uid);
         }
+
+        @Override
+        public void noteWakingAlarmBatch(long elapsedMillis, int... uids) {
+            noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, elapsedMillis, uids);
+        }
+    }
+
+    /**
+     * Reports any activity that could potentially have caused the CPU to wake up.
+     * Accepts a timestamp to allow free ordering between the event and its reporting.
+     *
+     * <p>
+     * This method can be called multiple times for the same wakeup and then all attribution
+     * reported will be unioned as long as all reports are made within a small amount of cpu uptime
+     * after the wakeup is reported to batterystats.
+     *
+     * @param subsystem The subsystem this activity should be attributed to.
+     * @param elapsedMillis The time when this activity happened in the elapsed timebase.
+     * @param uids The uid (or uids) that should be blamed for this activity.
+     */
+    void noteCpuWakingActivity(@CpuWakeupSubsystem int subsystem, long elapsedMillis, int... uids) {
+        Objects.requireNonNull(uids);
+        mHandler.post(() -> mCpuWakeupStats.noteWakingActivity(subsystem, elapsedMillis, uids));
     }
 
     @Override
@@ -1267,6 +1289,7 @@
         if (callingUid != Process.SYSTEM_UID) {
             throw new SecurityException("Calling uid " + callingUid + " is not system uid");
         }
+        final long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
 
         final SensorManager sm = mContext.getSystemService(SensorManager.class);
         final Sensor sensor = sm.getSensorByHandle(sensorHandle);
@@ -1275,10 +1298,12 @@
                     + " received in noteWakeupSensorEvent");
             return;
         }
-        Slog.i(TAG, "Sensor " + sensor + " wakeup event at " + elapsedNanos + " sent to uid "
-                + uid);
-        // TODO (b/275436924): Remove log and pipe to CpuWakeupStats for wakeup attribution
-        // This method should return as quickly as possible. Use mHandler#post to do longer work.
+        if (uid < 0) {
+            Slog.wtf(TAG, "Invalid uid " + uid + " for sensor event with sensor: " + sensor);
+            return;
+        }
+        // TODO (b/278319756): Also pipe in Sensor type for more usefulness.
+        noteCpuWakingActivity(BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR, elapsedMillis, uid);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 5edd434..462c938 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -77,7 +77,7 @@
 
     //------------------------------------------------------------
 
-    private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) {
+    private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(14) {
         {
             append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL);
             append(AudioDeviceInfo.TYPE_WIRED_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL);
@@ -91,7 +91,6 @@
             append(AudioDeviceInfo.TYPE_LINE_ANALOG, SpatializationMode.SPATIALIZER_TRANSAURAL);
             append(AudioDeviceInfo.TYPE_LINE_DIGITAL, SpatializationMode.SPATIALIZER_TRANSAURAL);
             append(AudioDeviceInfo.TYPE_AUX_LINE, SpatializationMode.SPATIALIZER_TRANSAURAL);
-            append(AudioDeviceInfo.TYPE_HEARING_AID, SpatializationMode.SPATIALIZER_BINAURAL);
             append(AudioDeviceInfo.TYPE_BLE_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL);
             append(AudioDeviceInfo.TYPE_BLE_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL);
             // assumption that BLE broadcast would be mostly consumed on headsets
diff --git a/services/core/java/com/android/server/biometrics/OWNERS b/services/core/java/com/android/server/biometrics/OWNERS
index cd281e0..1bf2aef 100644
--- a/services/core/java/com/android/server/biometrics/OWNERS
+++ b/services/core/java/com/android/server/biometrics/OWNERS
@@ -6,6 +6,10 @@
 jbolinger@google.com
 jeffpu@google.com
 joshmccloskey@google.com
+diyab@google.com
+austindelgado@google.com
+spdonghao@google.com
+wenhuiy@google.com
 
 firewall@google.com
 jasonsfchang@google.com
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
index 2653ce7..d9947dd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java
@@ -95,8 +95,10 @@
             }
         }
 
-        mRingBuffer.addApiCall("internal : onAuthSessionEnded(" + mUserId + ")");
-        clearSession();
+        if (mAuthOperations.isEmpty()) {
+            mRingBuffer.addApiCall("internal : onAuthSessionEnded(" + mUserId + ")");
+            clearSession();
+        }
     }
 
     private void clearSession() {
@@ -203,7 +205,7 @@
             return;
         }
         mAuthOperations.remove(sensorId);
-        if (mIsAuthenticating && mAuthOperations.isEmpty()) {
+        if (mIsAuthenticating) {
             endAuthSession();
         }
     }
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 299f865..378363c 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -1160,6 +1160,14 @@
         update();
     }
 
+    /**
+     * Convert a brightness float scale value to a nit value. Adjustments, such as RBC, are not
+     * applied. This is used when storing the brightness in nits for the default display and when
+     * passing the brightness value to follower displays.
+     *
+     * @param brightness The float scale value
+     * @return The nit value or -1f if no conversion is possible.
+     */
     public float convertToNits(float brightness) {
         if (mCurrentBrightnessMapper != null) {
             return mCurrentBrightnessMapper.convertToNits(brightness);
@@ -1168,6 +1176,30 @@
         }
     }
 
+    /**
+     * Convert a brightness float scale value to a nit value. Adjustments, such as RBC are applied.
+     * This is used when sending the brightness value to
+     * {@link com.android.server.display.BrightnessTracker}.
+     *
+     * @param brightness The float scale value
+     * @return The nit value or -1f if no conversion is possible.
+     */
+    public float convertToAdjustedNits(float brightness) {
+        if (mCurrentBrightnessMapper != null) {
+            return mCurrentBrightnessMapper.convertToAdjustedNits(brightness);
+        } else {
+            return -1.0f;
+        }
+    }
+
+    /**
+     * Convert a brightness nit value to a float scale value. It is assumed that the nit value
+     * provided does not have adjustments, such as RBC, applied.
+     *
+     * @param nits The nit value
+     * @return The float scale value or {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if no
+     * conversion is possible.
+     */
     public float convertToFloatScale(float nits) {
         if (mCurrentBrightnessMapper != null) {
             return mCurrentBrightnessMapper.convertToFloatScale(nits);
diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
index 3456e3e..df2a830 100644
--- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
+++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java
@@ -322,6 +322,14 @@
     public abstract float convertToNits(float brightness);
 
     /**
+     * Converts the provided brightness value to nits if possible. Adjustments, such as RBC are
+     * applied.
+     *
+     * Returns -1.0f if there's no available mapping for the brightness to nits.
+     */
+    public abstract float convertToAdjustedNits(float brightness);
+
+    /**
      * Converts the provided nit value to a float scale value if possible.
      *
      * Returns {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if there's no available mapping for
@@ -683,6 +691,11 @@
         }
 
         @Override
+        public float convertToAdjustedNits(float brightness) {
+            return -1.0f;
+        }
+
+        @Override
         public float convertToFloatScale(float nits) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
@@ -804,6 +817,14 @@
         // a brightness in nits.
         private Spline mBrightnessToNitsSpline;
 
+        // A spline mapping from nits with adjustments applied to the corresponding brightness
+        // value, normalized to the range [0, 1.0].
+        private Spline mAdjustedNitsToBrightnessSpline;
+
+        // A spline mapping from the system brightness value, normalized to the range [0, 1.0], to
+        // a brightness in nits with adjustments applied.
+        private Spline mBrightnessToAdjustedNitsSpline;
+
         // The default brightness configuration.
         private final BrightnessConfiguration mDefaultConfig;
 
@@ -843,6 +864,8 @@
             mNits = nits;
             mBrightness = brightness;
             computeNitsBrightnessSplines(mNits);
+            mAdjustedNitsToBrightnessSpline = mNitsToBrightnessSpline;
+            mBrightnessToAdjustedNitsSpline = mBrightnessToNitsSpline;
 
             mDefaultConfig = config;
             if (mLoggingEnabled) {
@@ -892,7 +915,7 @@
                 nits = mDisplayWhiteBalanceController.calculateAdjustedBrightnessNits(nits);
             }
 
-            float brightness = mNitsToBrightnessSpline.interpolate(nits);
+            float brightness = mAdjustedNitsToBrightnessSpline.interpolate(nits);
             // Correct the brightness according to the current application and its category, but
             // only if no user data point is set (as this will override the user setting).
             if (mUserLux == -1) {
@@ -930,6 +953,11 @@
         }
 
         @Override
+        public float convertToAdjustedNits(float brightness) {
+            return mBrightnessToAdjustedNitsSpline.interpolate(brightness);
+        }
+
+        @Override
         public float convertToFloatScale(float nits) {
             return mNitsToBrightnessSpline.interpolate(nits);
         }
@@ -989,7 +1017,13 @@
         @Override
         public void recalculateSplines(boolean applyAdjustment, float[] adjustedNits) {
             mBrightnessRangeAdjustmentApplied = applyAdjustment;
-            computeNitsBrightnessSplines(mBrightnessRangeAdjustmentApplied ? adjustedNits : mNits);
+            if (applyAdjustment) {
+                mAdjustedNitsToBrightnessSpline = Spline.createSpline(adjustedNits, mBrightness);
+                mBrightnessToAdjustedNitsSpline = Spline.createSpline(mBrightness, adjustedNits);
+            } else {
+                mAdjustedNitsToBrightnessSpline = mNitsToBrightnessSpline;
+                mBrightnessToAdjustedNitsSpline = mBrightnessToNitsSpline;
+            }
         }
 
         @Override
@@ -999,6 +1033,8 @@
             pw.println("  mBrightnessSpline=" + mBrightnessSpline);
             pw.println("  mNitsToBrightnessSpline=" + mNitsToBrightnessSpline);
             pw.println("  mBrightnessToNitsSpline=" + mBrightnessToNitsSpline);
+            pw.println("  mAdjustedNitsToBrightnessSpline=" + mAdjustedNitsToBrightnessSpline);
+            pw.println("  mAdjustedBrightnessToNitsSpline=" + mBrightnessToAdjustedNitsSpline);
             pw.println("  mMaxGamma=" + mMaxGamma);
             pw.println("  mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
             pw.println("  mUserLux=" + mUserLux);
@@ -1072,7 +1108,7 @@
                 float defaultNits = defaultSpline.interpolate(lux);
                 float longTermNits = currSpline.interpolate(lux);
                 float shortTermNits = mBrightnessSpline.interpolate(lux);
-                float brightness = mNitsToBrightnessSpline.interpolate(shortTermNits);
+                float brightness = mAdjustedNitsToBrightnessSpline.interpolate(shortTermNits);
 
                 String luxPrefix = (lux == mUserLux ? "^" : "");
                 String strLux = luxPrefix + toStrFloatForDump(lux);
@@ -1146,7 +1182,7 @@
             float[] defaultNits = defaultCurve.second;
             float[] defaultBrightness = new float[defaultNits.length];
             for (int i = 0; i < defaultBrightness.length; i++) {
-                defaultBrightness[i] = mNitsToBrightnessSpline.interpolate(defaultNits[i]);
+                defaultBrightness[i] = mAdjustedNitsToBrightnessSpline.interpolate(defaultNits[i]);
             }
             Pair<float[], float[]> curve = getAdjustedCurve(defaultLux, defaultBrightness, mUserLux,
                     mUserBrightness, mAutoBrightnessAdjustment, mMaxGamma);
@@ -1154,7 +1190,7 @@
             float[] brightness = curve.second;
             float[] nits = new float[brightness.length];
             for (int i = 0; i < nits.length; i++) {
-                nits[i] = mBrightnessToNitsSpline.interpolate(brightness[i]);
+                nits[i] = mBrightnessToAdjustedNitsSpline.interpolate(brightness[i]);
             }
             mBrightnessSpline = Spline.createSpline(lux, nits);
         }
@@ -1162,7 +1198,7 @@
         private float getUnadjustedBrightness(float lux) {
             Pair<float[], float[]> curve = mConfig.getCurve();
             Spline spline = Spline.createSpline(curve.first, curve.second);
-            return mNitsToBrightnessSpline.interpolate(spline.interpolate(lux));
+            return mAdjustedNitsToBrightnessSpline.interpolate(spline.interpolate(lux));
         }
 
         private float correctBrightness(float brightness, String packageName, int category) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 5d92c7f..26b6cb0 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1781,7 +1781,24 @@
         } else {
             configurePreferredDisplayModeLocked(display);
         }
-        addDisplayPowerControllerLocked(display);
+        DisplayPowerControllerInterface dpc = addDisplayPowerControllerLocked(display);
+
+        if (dpc != null) {
+            final int leadDisplayId = display.getLeadDisplayIdLocked();
+            updateDisplayPowerControllerLeaderLocked(dpc, leadDisplayId);
+
+            // Loop through all the displays and check if any should follow this one - it could be
+            // that the follower display was added before the lead display.
+            mLogicalDisplayMapper.forEachLocked(d -> {
+                if (d.getLeadDisplayIdLocked() == displayId) {
+                    DisplayPowerControllerInterface followerDpc =
+                            mDisplayPowerControllers.get(d.getDisplayIdLocked());
+                    if (followerDpc != null) {
+                        updateDisplayPowerControllerLeaderLocked(followerDpc, displayId);
+                    }
+                }
+            });
+        }
 
         mDisplayStates.append(displayId, Display.STATE_UNKNOWN);
 
@@ -1832,8 +1849,8 @@
         }
     }
 
-    private void updateDisplayPowerControllerLeaderLocked(DisplayPowerControllerInterface dpc,
-            int leadDisplayId) {
+    private void updateDisplayPowerControllerLeaderLocked(
+            @NonNull DisplayPowerControllerInterface dpc, int leadDisplayId) {
         if (dpc.getLeadDisplayId() == leadDisplayId) {
             // Lead display hasn't changed, nothing to do.
             return;
@@ -1851,9 +1868,11 @@
 
         // And then, if it's following, register it with the new one.
         if (leadDisplayId != Layout.NO_LEAD_DISPLAY) {
-            final DisplayPowerControllerInterface newLead =
+            final DisplayPowerControllerInterface newLeader =
                     mDisplayPowerControllers.get(leadDisplayId);
-            newLead.addDisplayBrightnessFollower(dpc);
+            if (newLeader != null) {
+                newLeader.addDisplayBrightnessFollower(dpc);
+            }
         }
     }
 
@@ -1872,6 +1891,7 @@
         final DisplayPowerControllerInterface dpc =
                 mDisplayPowerControllers.removeReturnOld(displayId);
         if (dpc != null) {
+            updateDisplayPowerControllerLeaderLocked(dpc, Layout.NO_LEAD_DISPLAY);
             dpc.stop();
         }
         mDisplayStates.delete(displayId);
@@ -3062,10 +3082,11 @@
     }
 
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-    private void addDisplayPowerControllerLocked(LogicalDisplay display) {
+    private DisplayPowerControllerInterface addDisplayPowerControllerLocked(
+            LogicalDisplay display) {
         if (mPowerHandler == null) {
             // initPowerManagement has not yet been called.
-            return;
+            return null;
         }
 
         if (mBrightnessTracker == null && display.getDisplayIdLocked() == Display.DEFAULT_DISPLAY) {
@@ -3086,7 +3107,7 @@
         if (hbmMetadata == null) {
             Slog.wtf(TAG, "High Brightness Mode Metadata is null in DisplayManagerService for "
                     + "display: " + display.getDisplayIdLocked());
-            return;
+            return null;
         }
         if (DeviceConfig.getBoolean("display_manager",
                 "use_newly_structured_display_power_controller", true)) {
@@ -3101,6 +3122,7 @@
                     () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted);
         }
         mDisplayPowerControllers.append(display.getDisplayIdLocked(), displayPowerController);
+        return displayPowerController;
     }
 
     private void handleBrightnessChange(LogicalDisplay display) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 1acc208..f1efec0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -789,6 +789,17 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private void clearDisplayBrightnessFollowersLocked() {
+        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    /* ambientLux= */ 0), mClock.uptimeMillis());
+        }
+        mDisplayBrightnessFollowers.clear();
+    }
+
     @Nullable
     @Override
     public ParceledListSlice<AmbientBrightnessDayStats> getAmbientBrightnessStats(
@@ -946,6 +957,8 @@
     @Override
     public void stop() {
         synchronized (mLock) {
+            clearDisplayBrightnessFollowersLocked();
+
             mStopped = true;
             Message msg = mHandler.obtainMessage(MSG_STOP);
             mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
@@ -1039,7 +1052,7 @@
         noteScreenBrightness(mPowerState.getScreenBrightness());
 
         // Initialize all of the brightness tracking state
-        final float brightness = convertToNits(mPowerState.getScreenBrightness());
+        final float brightness = convertToAdjustedNits(mPowerState.getScreenBrightness());
         if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
         }
@@ -2698,7 +2711,7 @@
 
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean wasShortTermModelActive) {
-        final float brightnessInNits = convertToNits(brightness);
+        final float brightnessInNits = convertToAdjustedNits(brightness);
         if (mUseAutoBrightness && brightnessInNits >= 0.0f
                 && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
@@ -2722,6 +2735,13 @@
         return mAutomaticBrightnessController.convertToNits(brightness);
     }
 
+    private float convertToAdjustedNits(float brightness) {
+        if (mAutomaticBrightnessController == null) {
+            return -1f;
+        }
+        return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
+    }
+
     private float convertToFloatScale(float nits) {
         if (mAutomaticBrightnessController == null) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
@@ -3075,16 +3095,16 @@
         int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
         float appliedHbmMaxNits =
                 event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : convertToNits(event.getHbmMax());
+                ? -1f : convertToAdjustedNits(event.getHbmMax());
         // thermalCapNits set to -1 if not currently capping max brightness
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : convertToNits(event.getThermalMax());
+                ? -1f : convertToAdjustedNits(event.getThermalMax());
 
         if (mIsDisplayInternal) {
             FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    convertToNits(event.getInitialBrightness()),
-                    convertToNits(event.getBrightness()),
+                    convertToAdjustedNits(event.getInitialBrightness()),
+                    convertToAdjustedNits(event.getBrightness()),
                     event.getLux(),
                     event.getPhysicalDisplayId(),
                     event.wasShortTermModelActive(),
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index b36aede..59e112e 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -764,6 +764,8 @@
     @Override
     public void stop() {
         synchronized (mLock) {
+            clearDisplayBrightnessFollowersLocked();
+
             mStopped = true;
             Message msg = mHandler.obtainMessage(MSG_STOP);
             mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
@@ -854,7 +856,7 @@
         noteScreenBrightness(mPowerState.getScreenBrightness());
 
         // Initialize all of the brightness tracking state
-        final float brightness = mDisplayBrightnessController.convertToNits(
+        final float brightness = mDisplayBrightnessController.convertToAdjustedNits(
                 mPowerState.getScreenBrightness());
         if (mBrightnessTracker != null && brightness >= PowerManager.BRIGHTNESS_MIN) {
             mBrightnessTracker.start(brightness);
@@ -2165,7 +2167,8 @@
 
     private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated,
             boolean wasShortTermModelActive) {
-        final float brightnessInNits = mDisplayBrightnessController.convertToNits(brightness);
+        final float brightnessInNits =
+                mDisplayBrightnessController.convertToAdjustedNits(brightness);
         if (mAutomaticBrightnessStrategy.shouldUseAutoBrightness() && brightnessInNits >= 0.0f
                 && mAutomaticBrightnessController != null && mBrightnessTracker != null) {
             // We only want to track changes on devices that can actually map the display backlight
@@ -2200,6 +2203,17 @@
         }
     }
 
+    @GuardedBy("mLock")
+    private void clearDisplayBrightnessFollowersLocked() {
+        for (int i = 0; i < mDisplayBrightnessFollowers.size(); i++) {
+            DisplayPowerControllerInterface follower = mDisplayBrightnessFollowers.valueAt(i);
+            mHandler.postAtTime(() -> follower.setBrightnessToFollow(
+                    PowerManager.BRIGHTNESS_INVALID_FLOAT, /* nits= */ -1,
+                    /* ambientLux= */ 0), mClock.uptimeMillis());
+        }
+        mDisplayBrightnessFollowers.clear();
+    }
+
     @Override
     public void dump(final PrintWriter pw) {
         synchronized (mLock) {
@@ -2438,15 +2452,16 @@
         int appliedRbcStrength  = event.isRbcEnabled() ? event.getRbcStrength() : -1;
         float appliedHbmMaxNits =
                 event.getHbmMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF
-                ? -1f : mDisplayBrightnessController.convertToNits(event.getHbmMax());
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getHbmMax());
         // thermalCapNits set to -1 if not currently capping max brightness
         float appliedThermalCapNits =
                 event.getThermalMax() == PowerManager.BRIGHTNESS_MAX
-                ? -1f : mDisplayBrightnessController.convertToNits(event.getThermalMax());
+                ? -1f : mDisplayBrightnessController.convertToAdjustedNits(event.getThermalMax());
         if (mIsDisplayInternal) {
             FrameworkStatsLog.write(FrameworkStatsLog.DISPLAY_BRIGHTNESS_CHANGED,
-                    mDisplayBrightnessController.convertToNits(event.getInitialBrightness()),
-                    mDisplayBrightnessController.convertToNits(event.getBrightness()),
+                    mDisplayBrightnessController
+                            .convertToAdjustedNits(event.getInitialBrightness()),
+                    mDisplayBrightnessController.convertToAdjustedNits(event.getBrightness()),
                     event.getLux(),
                     event.getPhysicalDisplayId(),
                     event.wasShortTermModelActive(),
diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
index 2916fef..a3f8c4d 100644
--- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
+++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessController.java
@@ -312,7 +312,10 @@
     }
 
     /**
-     * Convert a brightness float scale value to a nit value.
+     * Convert a brightness float scale value to a nit value. Adjustments, such as RBC, are not
+     * applied. This is used when storing the brightness in nits for the default display and when
+     * passing the brightness value to follower displays.
+     *
      * @param brightness The float scale value
      * @return The nit value or -1f if no conversion is possible.
      */
@@ -324,7 +327,24 @@
     }
 
     /**
-     * Convert a brightness nit value to a float scale value.
+     * Convert a brightness float scale value to a nit value. Adjustments, such as RBC are applied.
+     * This is used when sending the brightness value to
+     * {@link com.android.server.display.BrightnessTracker}.
+     *
+     * @param brightness The float scale value
+     * @return The nit value or -1f if no conversion is possible.
+     */
+    public float convertToAdjustedNits(float brightness) {
+        if (mAutomaticBrightnessController == null) {
+            return -1f;
+        }
+        return mAutomaticBrightnessController.convertToAdjustedNits(brightness);
+    }
+
+    /**
+     * Convert a brightness nit value to a float scale value. It is assumed that the nit value
+     * provided does not have adjustments, such as RBC, applied.
+     *
      * @param nits The nit value
      * @return The float scale value or {@link PowerManager.BRIGHTNESS_INVALID_FLOAT} if no
      * conversion is possible.
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index 73440b7..12fc263 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -140,34 +140,39 @@
      * The services that have been bound by us. If the service is also connected, it will also
      * be in {@link #mServices}.
      */
+    @GuardedBy("mMutex")
     private final ArrayList<Pair<ComponentName, Integer>> mServicesBound = new ArrayList<>();
+    @GuardedBy("mMutex")
     private final ArraySet<Pair<ComponentName, Integer>> mServicesRebinding = new ArraySet<>();
     // we need these packages to be protected because classes that inherit from it need to see it
     protected final Object mDefaultsLock = new Object();
+    @GuardedBy("mDefaultsLock")
     protected final ArraySet<ComponentName> mDefaultComponents = new ArraySet<>();
+    @GuardedBy("mDefaultsLock")
     protected final ArraySet<String> mDefaultPackages = new ArraySet<>();
 
     // lists the component names of all enabled (and therefore potentially connected)
     // app services for current profiles.
-    private ArraySet<ComponentName> mEnabledServicesForCurrentProfiles
-            = new ArraySet<>();
+    @GuardedBy("mMutex")
+    private final ArraySet<ComponentName> mEnabledServicesForCurrentProfiles = new ArraySet<>();
     // Just the packages from mEnabledServicesForCurrentProfiles
-    private ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
+    @GuardedBy("mMutex")
+    private final ArraySet<String> mEnabledServicesPackageNames = new ArraySet<>();
     // Per user id, list of enabled packages that have nevertheless asked not to be run
-    private final android.util.SparseSetArray<ComponentName> mSnoozing =
-            new android.util.SparseSetArray<>();
+    @GuardedBy("mSnoozing")
+    private final SparseSetArray<ComponentName> mSnoozing = new SparseSetArray<>();
 
     // List of approved packages or components (by user, then by primary/secondary) that are
     // allowed to be bound as managed services. A package or component appearing in this list does
     // not mean that we are currently bound to said package/component.
+    @GuardedBy("mApproved")
     protected final ArrayMap<Integer, ArrayMap<Boolean, ArraySet<String>>> mApproved =
             new ArrayMap<>();
-
     // List of packages or components (by user) that are configured to be enabled/disabled
     // explicitly by the user
     @GuardedBy("mApproved")
     protected ArrayMap<Integer, ArraySet<String>> mUserSetServices = new ArrayMap<>();
-
+    @GuardedBy("mApproved")
     protected ArrayMap<Integer, Boolean> mIsUserChanged = new ArrayMap<>();
 
     // True if approved services are stored in xml, not settings.
@@ -262,20 +267,18 @@
     @NonNull
     ArrayMap<Boolean, ArrayList<ComponentName>> resetComponents(String packageName, int userId) {
         // components that we want to enable
-        ArrayList<ComponentName> componentsToEnable =
-                new ArrayList<>(mDefaultComponents.size());
-
+        ArrayList<ComponentName> componentsToEnable;
         // components that were removed
-        ArrayList<ComponentName> disabledComponents =
-                new ArrayList<>(mDefaultComponents.size());
-
+        ArrayList<ComponentName> disabledComponents;
         // all components that are enabled now
-        ArraySet<ComponentName> enabledComponents =
-                new ArraySet<>(getAllowedComponents(userId));
+        ArraySet<ComponentName> enabledComponents = new ArraySet<>(getAllowedComponents(userId));
 
         boolean changed = false;
 
         synchronized (mDefaultsLock) {
+            componentsToEnable = new ArrayList<>(mDefaultComponents.size());
+            disabledComponents = new ArrayList<>(mDefaultComponents.size());
+
             // record all components that are enabled but should not be by default
             for (int i = 0; i < mDefaultComponents.size() && enabledComponents.size() > 0; i++) {
                 ComponentName currentDefault = mDefaultComponents.valueAt(i);
@@ -374,14 +377,14 @@
             }
         }
 
-        pw.println("    All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size()
-                + ") enabled for current profiles:");
-        for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
-            if (filter != null && !filter.matches(cmpt)) continue;
-            pw.println("      " + cmpt);
-        }
-
         synchronized (mMutex) {
+            pw.println("    All " + getCaption() + "s (" + mEnabledServicesForCurrentProfiles.size()
+                    + ") enabled for current profiles:");
+            for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
+                if (filter != null && !filter.matches(cmpt)) continue;
+                pw.println("      " + cmpt);
+            }
+
             pw.println("    Live " + getCaption() + "s (" + mServices.size() + "):");
             for (ManagedServiceInfo info : mServices) {
                 if (filter != null && !filter.matches(info.component)) continue;
@@ -434,12 +437,12 @@
             }
         }
 
-        for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
-            if (filter != null && !filter.matches(cmpt)) continue;
-            cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
-        }
 
         synchronized (mMutex) {
+            for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
+                if (filter != null && !filter.matches(cmpt)) continue;
+                cmpt.dumpDebug(proto, ManagedServicesProto.ENABLED);
+            }
             for (ManagedServiceInfo info : mServices) {
                 if (filter != null && !filter.matches(info.component)) continue;
                 info.dumpDebug(proto, ManagedServicesProto.LIVE_SERVICES, this);
@@ -677,7 +680,9 @@
                         if (isUserChanged == null) { //NLS
                             userSetComponent = TextUtils.emptyIfNull(userSetComponent);
                         } else { //NAS
-                            mIsUserChanged.put(resolvedUserId, Boolean.valueOf(isUserChanged));
+                            synchronized (mApproved) {
+                                mIsUserChanged.put(resolvedUserId, Boolean.valueOf(isUserChanged));
+                            }
                             userSetComponent = Boolean.valueOf(isUserChanged) ? approved : "";
                         }
                     } else {
@@ -688,7 +693,9 @@
                             if (isUserChanged_Old != null && Boolean.valueOf(isUserChanged_Old)) {
                                 //user_set = true
                                 userSetComponent = approved;
-                                mIsUserChanged.put(resolvedUserId, true);
+                                synchronized (mApproved) {
+                                    mIsUserChanged.put(resolvedUserId, true);
+                                }
                                 needUpgradeUserset = false;
                             } else {
                                 userSetComponent = "";
@@ -724,8 +731,11 @@
     }
 
     void upgradeDefaultsXmlVersion() {
-        // check if any defaults are loaded
-        int defaultsSize = mDefaultComponents.size() + mDefaultPackages.size();
+        int defaultsSize;
+        synchronized (mDefaultsLock) {
+            // check if any defaults are loaded
+            defaultsSize = mDefaultComponents.size() + mDefaultPackages.size();
+        }
         if (defaultsSize == 0) {
             // load defaults from current allowed
             if (this.mApprovalLevel == APPROVAL_BY_COMPONENT) {
@@ -741,8 +751,10 @@
                 }
             }
         }
+        synchronized (mDefaultsLock) {
+            defaultsSize = mDefaultComponents.size() + mDefaultPackages.size();
+        }
         // if no defaults are loaded, then load from config
-        defaultsSize = mDefaultComponents.size() + mDefaultPackages.size();
         if (defaultsSize == 0) {
             loadDefaultsFromConfig();
         }
@@ -806,7 +818,9 @@
     }
 
     protected boolean isComponentEnabledForPackage(String pkg) {
-        return mEnabledServicesPackageNames.contains(pkg);
+        synchronized (mMutex) {
+            return mEnabledServicesPackageNames.contains(pkg);
+        }
     }
 
     protected void setPackageOrComponentEnabled(String pkgOrComponent, int userId,
@@ -959,9 +973,13 @@
     }
 
     public void onPackagesChanged(boolean removingPackage, String[] pkgList, int[] uidList) {
-        if (DEBUG) Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage
-                + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
-                + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames);
+        if (DEBUG) {
+            synchronized (mMutex) {
+                Slog.d(TAG, "onPackagesChanged removingPackage=" + removingPackage
+                        + " pkgList=" + (pkgList == null ? null : Arrays.asList(pkgList))
+                        + " mEnabledServicesPackageNames=" + mEnabledServicesPackageNames);
+            }
+        }
 
         if (pkgList != null && (pkgList.length > 0)) {
             boolean anyServicesInvolved = false;
@@ -975,7 +993,7 @@
                 }
             }
             for (String pkgName : pkgList) {
-                if (mEnabledServicesPackageNames.contains(pkgName)) {
+                if (isComponentEnabledForPackage(pkgName)) {
                     anyServicesInvolved = true;
                 }
                 if (uidList != null && uidList.length > 0) {
@@ -1299,9 +1317,11 @@
             }
 
             final Set<ComponentName> add = new HashSet<>(userComponents);
-            ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
-            if (snoozed != null) {
-                add.removeAll(snoozed);
+            synchronized (mSnoozing) {
+                ArraySet<ComponentName> snoozed = mSnoozing.get(userId);
+                if (snoozed != null) {
+                    add.removeAll(snoozed);
+                }
             }
 
             componentsToBind.put(userId, add);
@@ -1605,9 +1625,12 @@
         }
     }
 
+    @VisibleForTesting
     boolean isBound(ComponentName cn, int userId) {
         final Pair<ComponentName, Integer> servicesBindingTag = Pair.create(cn, userId);
-        return mServicesBound.contains(servicesBindingTag);
+        synchronized (mMutex) {
+            return mServicesBound.contains(servicesBindingTag);
+        }
     }
 
     protected boolean isBoundOrRebinding(final ComponentName cn, final int userId) {
@@ -1833,7 +1856,9 @@
         public boolean isEnabledForCurrentProfiles() {
             if (this.isSystem) return true;
             if (this.connection == null) return false;
-            return mEnabledServicesForCurrentProfiles.contains(this.component);
+            synchronized (mMutex) {
+                return mEnabledServicesForCurrentProfiles.contains(this.component);
+            }
         }
 
         /**
@@ -1877,7 +1902,9 @@
 
     /** convenience method for looking in mEnabledServicesForCurrentProfiles */
     public boolean isComponentEnabledForCurrentProfiles(ComponentName component) {
-        return mEnabledServicesForCurrentProfiles.contains(component);
+        synchronized (mMutex) {
+            return mEnabledServicesForCurrentProfiles.contains(component);
+        }
     }
 
     public static class UserProfiles {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index 2704f56..0205280 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -393,7 +393,7 @@
     public static boolean isPrivacySafetyLabelChangeNotificationsEnabled(Context context) {
         PackageManager packageManager = context.getPackageManager();
         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
-                SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, false)
+                SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, true)
             && !packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
             && !packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
             && !packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index fc6b4e9..4094b1a 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3484,6 +3484,11 @@
                     interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/);
                 }
                 return true;
+            case KeyEvent.KEYCODE_ESCAPE:
+                if (down && repeatCount == 0) {
+                    mContext.closeSystemDialogs();
+                }
+                return true;
         }
 
         return false;
diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
index 1d63489..eb6d28e 100644
--- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
@@ -17,6 +17,8 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI;
@@ -54,10 +56,11 @@
  */
 public class CpuWakeupStats {
     private static final String TAG = "CpuWakeupStats";
-
     private static final String SUBSYSTEM_ALARM_STRING = "Alarm";
     private static final String SUBSYSTEM_WIFI_STRING = "Wifi";
     private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger";
+    private static final String SUBSYSTEM_SENSOR_STRING = "Sensor";
+    private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data";
     private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution";
     @VisibleForTesting
     static final long WAKEUP_REASON_HALF_WINDOW_MS = 500;
@@ -111,6 +114,10 @@
                 return FrameworkStatsLog.KERNEL_WAKEUP_ATTRIBUTED__REASON__WIFI;
             case CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER:
                 return FrameworkStatsLog.KERNEL_WAKEUP_ATTRIBUTED__REASON__SOUND_TRIGGER;
+            case CPU_WAKEUP_SUBSYSTEM_SENSOR:
+                return FrameworkStatsLog.KERNEL_WAKEUP_ATTRIBUTED__REASON__SENSOR;
+            case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
+                return FrameworkStatsLog.KERNEL_WAKEUP_ATTRIBUTED__REASON__CELLULAR_DATA;
         }
         return FrameworkStatsLog.KERNEL_WAKEUP_ATTRIBUTED__REASON__UNKNOWN;
     }
@@ -542,6 +549,10 @@
                 return CPU_WAKEUP_SUBSYSTEM_WIFI;
             case SUBSYSTEM_SOUND_TRIGGER_STRING:
                 return CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
+            case SUBSYSTEM_SENSOR_STRING:
+                return CPU_WAKEUP_SUBSYSTEM_SENSOR;
+            case SUBSYSTEM_CELLULAR_DATA_STRING:
+                return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
         }
         return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
     }
@@ -554,6 +565,10 @@
                 return SUBSYSTEM_WIFI_STRING;
             case CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER:
                 return SUBSYSTEM_SOUND_TRIGGER_STRING;
+            case CPU_WAKEUP_SUBSYSTEM_SENSOR:
+                return SUBSYSTEM_SENSOR_STRING;
+            case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
+                return SUBSYSTEM_CELLULAR_DATA_STRING;
             case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
                 return "Unknown";
         }
diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
index 2007079..0ca5603 100644
--- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java
@@ -121,7 +121,7 @@
             impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
         } else if (getAvailableRollback(failedPackage) != null) {
             // Rollback is available, we may get a callback into #execute
-            impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
+            impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_60;
         } else if (anyRollbackAvailable) {
             // If any rollbacks are available, we will commit them
             impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
index cd1a968..97e4636 100644
--- a/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningService.java
@@ -19,14 +19,18 @@
 import android.content.Context;
 import android.os.Binder;
 import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.security.rkp.IGetRegistrationCallback;
 import android.security.rkp.IRemoteProvisioning;
 import android.security.rkp.service.RegistrationProxy;
 import android.util.Log;
 
+import com.android.internal.util.DumpUtils;
 import com.android.server.SystemService;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.time.Duration;
 import java.util.concurrent.Executor;
 
@@ -97,5 +101,18 @@
                 Binder.restoreCallingIdentity(callingIdentity);
             }
         }
+
+        @Override
+        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
+            new RemoteProvisioningShellCommand().dump(pw);
+        }
+
+        @Override
+        public int handleShellCommand(ParcelFileDescriptor in, ParcelFileDescriptor out,
+                ParcelFileDescriptor err, String[] args) {
+            return new RemoteProvisioningShellCommand().exec(this, in.getFileDescriptor(),
+                    out.getFileDescriptor(), err.getFileDescriptor(), args);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java b/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java
new file mode 100644
index 0000000..71eca69
--- /dev/null
+++ b/services/core/java/com/android/server/security/rkp/RemoteProvisioningShellCommand.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security.rkp;
+
+import android.hardware.security.keymint.DeviceInfo;
+import android.hardware.security.keymint.IRemotelyProvisionedComponent;
+import android.hardware.security.keymint.MacedPublicKey;
+import android.hardware.security.keymint.ProtectedData;
+import android.hardware.security.keymint.RpcHardwareInfo;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ShellCommand;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.util.Base64;
+
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.Map;
+import co.nstant.in.cbor.model.SimpleValue;
+import co.nstant.in.cbor.model.UnsignedInteger;
+
+class RemoteProvisioningShellCommand extends ShellCommand {
+    private static final String USAGE = "usage: cmd remote_provisioning SUBCOMMAND [ARGS]\n"
+            + "help\n"
+            + "  Show this message.\n"
+            + "dump\n"
+            + "  Dump service diagnostics.\n"
+            + "list [--min-version MIN_VERSION]\n"
+            + "  List the names of the IRemotelyProvisionedComponent instances.\n"
+            + "csr [--challenge CHALLENGE] NAME\n"
+            + "  Generate and print a base64-encoded CSR from the named\n"
+            + "  IRemotelyProvisionedComponent. A base64-encoded challenge can be provided,\n"
+            + "  or else it defaults to an empty challenge.\n";
+
+    @VisibleForTesting
+    static final String EEK_ED25519_BASE64 = "goRDoQEnoFgqpAEBAycgBiFYIJm57t1e5FL2hcZMYtw+YatXSH11N"
+            + "ymtdoAy0rPLY1jZWEAeIghLpLekyNdOAw7+uK8UTKc7b6XN3Np5xitk/pk5r3bngPpmAIUNB5gqrJFcpyUUS"
+            + "QY0dcqKJ3rZ41pJ6wIDhEOhASegWE6lAQECWCDQrsEVyirPc65rzMvRlh1l6LHd10oaN7lDOpfVmd+YCAM4G"
+            + "CAEIVggvoXnRsSjQlpA2TY6phXQLFh+PdwzAjLS/F4ehyVfcmBYQJvPkOIuS6vRGLEOjl0gJ0uEWP78MpB+c"
+            + "gWDvNeCvvpkeC1UEEvAMb9r6B414vAtzmwvT/L1T6XUg62WovGHWAQ=";
+
+    @VisibleForTesting
+    static final String EEK_P256_BASE64 = "goRDoQEmoFhNpQECAyYgASFYIPcUituX9MxT79JkEcTjdR9mH6RxDGzP"
+            + "+glGgHSHVPKtIlggXn9b9uzk9hnM/xM3/Q+hyJPbGAZ2xF3m12p3hsMtr49YQC+XjkL7vgctlUeFR5NAsB/U"
+            + "m0ekxESp8qEHhxDHn8sR9L+f6Dvg5zRMFfx7w34zBfTRNDztAgRgehXgedOK/ySEQ6EBJqBYcaYBAgJYIDVz"
+            + "tz+gioCJsSZn6ct8daGvAmH8bmUDkTvTS30UlD5GAzgYIAEhWCDgQc8vDzQPHDMsQbDP1wwwVTXSHmpHE0su"
+            + "0UiWfiScaCJYIB/ORcX7YbqBIfnlBZubOQ52hoZHuB4vRfHOr9o/gGjbWECMs7p+ID4ysGjfYNEdffCsOI5R"
+            + "vP9s4Wc7Snm8Vnizmdh8igfY2rW1f3H02GvfMyc0e2XRKuuGmZirOrSAqr1Q";
+
+    private static final int ERROR = -1;
+    private static final int SUCCESS = 0;
+
+    private final Injector mInjector;
+
+    RemoteProvisioningShellCommand() {
+        this(new Injector());
+    }
+
+    @VisibleForTesting
+    RemoteProvisioningShellCommand(Injector injector) {
+        mInjector = injector;
+    }
+
+    @Override
+    public void onHelp() {
+        getOutPrintWriter().print(USAGE);
+    }
+
+    @Override
+    @SuppressWarnings("CatchAndPrintStackTrace")
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+        try {
+            switch (cmd) {
+                case "list":
+                    return list();
+                case "csr":
+                    return csr();
+                default:
+                    return handleDefaultCommands(cmd);
+            }
+        } catch (Exception e) {
+            e.printStackTrace(getErrPrintWriter());
+            return ERROR;
+        }
+    }
+
+    @SuppressWarnings("CatchAndPrintStackTrace")
+    void dump(PrintWriter pw) {
+        try {
+            IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+            for (String name : mInjector.getIrpcNames()) {
+                ipw.println(name + ":");
+                ipw.increaseIndent();
+                dumpRpcInstance(ipw, name);
+                ipw.decreaseIndent();
+            }
+        } catch (Exception e) {
+            e.printStackTrace(pw);
+        }
+    }
+
+    private void dumpRpcInstance(PrintWriter pw, String name) throws RemoteException {
+        RpcHardwareInfo info = mInjector.getIrpcBinder(name).getHardwareInfo();
+        pw.println("hwVersion=" + info.versionNumber);
+        pw.println("rpcAuthorName=" + info.rpcAuthorName);
+        if (info.versionNumber < 3) {
+            pw.println("supportedEekCurve=" + info.supportedEekCurve);
+        }
+        pw.println("uniqueId=" + info.uniqueId);
+        pw.println("supportedNumKeysInCsr=" + info.supportedNumKeysInCsr);
+    }
+
+    private int list() throws RemoteException {
+        for (String name : mInjector.getIrpcNames()) {
+            getOutPrintWriter().println(name);
+        }
+        return SUCCESS;
+    }
+
+    private int csr() throws RemoteException, CborException {
+        byte[] challenge = {};
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "--challenge":
+                    challenge = Base64.getDecoder().decode(getNextArgRequired());
+                    break;
+                default:
+                    getErrPrintWriter().println("error: unknown option");
+                    return ERROR;
+            }
+        }
+        String name = getNextArgRequired();
+
+        IRemotelyProvisionedComponent binder = mInjector.getIrpcBinder(name);
+        RpcHardwareInfo info = binder.getHardwareInfo();
+        MacedPublicKey[] emptyKeys = new MacedPublicKey[] {};
+        byte[] csrBytes;
+        switch (info.versionNumber) {
+            case 1:
+            case 2:
+                DeviceInfo deviceInfo = new DeviceInfo();
+                ProtectedData protectedData = new ProtectedData();
+                byte[] eek = getEekChain(info.supportedEekCurve);
+                byte[] keysToSignMac = binder.generateCertificateRequest(
+                        /*testMode=*/false, emptyKeys, eek, challenge, deviceInfo, protectedData);
+                csrBytes = composeCertificateRequestV1(
+                        deviceInfo, challenge, protectedData, keysToSignMac);
+                break;
+            case 3:
+                csrBytes = binder.generateCertificateRequestV2(emptyKeys, challenge);
+                break;
+            default:
+                getErrPrintWriter().println("error: unsupported hwVersion: " + info.versionNumber);
+                return ERROR;
+        }
+        getOutPrintWriter().println(Base64.getEncoder().encodeToString(csrBytes));
+        return SUCCESS;
+    }
+
+    private byte[] getEekChain(int supportedEekCurve) {
+        switch (supportedEekCurve) {
+            case RpcHardwareInfo.CURVE_25519:
+                return Base64.getDecoder().decode(EEK_ED25519_BASE64);
+            case RpcHardwareInfo.CURVE_P256:
+                return Base64.getDecoder().decode(EEK_P256_BASE64);
+            default:
+                throw new IllegalArgumentException("unsupported EEK curve: " + supportedEekCurve);
+        }
+    }
+
+    private byte[] composeCertificateRequestV1(DeviceInfo deviceInfo, byte[] challenge,
+            ProtectedData protectedData, byte[] keysToSignMac) throws CborException {
+        Array info = new Array()
+                .add(decode(deviceInfo.deviceInfo))
+                .add(new Map());
+
+        // COSE_Signature with the hmac-sha256 algorithm and without a payload.
+        Array mac = new Array()
+                .add(new ByteString(encode(
+                            new Map().put(new UnsignedInteger(1), new UnsignedInteger(5)))))
+                .add(new Map())
+                .add(SimpleValue.NULL)
+                .add(new ByteString(keysToSignMac));
+
+        Array csr = new Array()
+                .add(info)
+                .add(new ByteString(challenge))
+                .add(decode(protectedData.protectedData))
+                .add(mac);
+
+        return encode(csr);
+    }
+
+    private byte[] encode(DataItem item) throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(item);
+        return baos.toByteArray();
+    }
+
+    private DataItem decode(byte[] data) throws CborException {
+        ByteArrayInputStream bais = new ByteArrayInputStream(data);
+        return new CborDecoder(bais).decodeNext();
+    }
+
+    @VisibleForTesting
+    static class Injector {
+        String[] getIrpcNames() {
+            return ServiceManager.getDeclaredInstances(IRemotelyProvisionedComponent.DESCRIPTOR);
+        }
+
+        IRemotelyProvisionedComponent getIrpcBinder(String name) {
+            String irpc = IRemotelyProvisionedComponent.DESCRIPTOR + "/" + name;
+            IRemotelyProvisionedComponent binder =
+                    IRemotelyProvisionedComponent.Stub.asInterface(
+                            ServiceManager.waitForDeclaredService(irpc));
+            if (binder == null) {
+                throw new IllegalArgumentException("failed to find " + irpc);
+            }
+            return binder;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index 0ce17de..6eded1a 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -171,6 +171,8 @@
 import android.util.SparseIntArray;
 import android.util.StatsEvent;
 import android.util.proto.ProtoOutputStream;
+import android.uwb.UwbActivityEnergyInfo;
+import android.uwb.UwbManager;
 import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
@@ -346,6 +348,7 @@
     private StorageManager mStorageManager;
     private WifiManager mWifiManager;
     private TelephonyManager mTelephony;
+    private UwbManager mUwbManager;
     private SubscriptionManager mSubscriptionManager;
     private NetworkStatsManager mNetworkStatsManager;
 
@@ -415,6 +418,7 @@
     private final Object mWifiActivityInfoLock = new Object();
     private final Object mModemActivityInfoLock = new Object();
     private final Object mBluetoothActivityInfoLock = new Object();
+    private final Object mUwbActivityInfoLock = new Object();
     private final Object mSystemElapsedRealtimeLock = new Object();
     private final Object mSystemUptimeLock = new Object();
     private final Object mProcessMemoryStateLock = new Object();
@@ -537,6 +541,10 @@
                         synchronized (mBluetoothActivityInfoLock) {
                             return pullBluetoothActivityInfoLocked(atomTag, data);
                         }
+                    case FrameworkStatsLog.UWB_ACTIVITY_INFO:
+                        synchronized (mUwbActivityInfoLock) {
+                            return pullUwbActivityInfoLocked(atomTag, data);
+                        }
                     case FrameworkStatsLog.SYSTEM_ELAPSED_REALTIME:
                         synchronized (mSystemElapsedRealtimeLock) {
                             return pullSystemElapsedRealtimeLocked(atomTag, data);
@@ -778,8 +786,12 @@
                 registerEventListeners();
             });
         } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
-            // Network stats related pullers can only be initialized after service is ready.
-            BackgroundThread.getHandler().post(() -> initAndRegisterNetworkStatsPullers());
+            BackgroundThread.getHandler().post(() -> {
+                // Network stats related pullers can only be initialized after service is ready.
+                initAndRegisterNetworkStatsPullers();
+                // For services that are not ready at boot phase PHASE_SYSTEM_SERVICES_READY
+                initAndRegisterDeferredPullers();
+            });
         }
     }
 
@@ -990,6 +1002,12 @@
         registerOemManagedBytesTransfer();
     }
 
+    private void initAndRegisterDeferredPullers() {
+        mUwbManager = mContext.getSystemService(UwbManager.class);
+
+        registerUwbActivityInfo();
+    }
+
     private IThermalService getIThermalService() {
         synchronized (mThermalLock) {
             if (mThermalService == null) {
@@ -2152,6 +2170,46 @@
         return StatsManager.PULL_SUCCESS;
     }
 
+    private void registerUwbActivityInfo() {
+        int tagId = FrameworkStatsLog.UWB_ACTIVITY_INFO;
+        mStatsManager.setPullAtomCallback(
+                tagId,
+                null, // use default PullAtomMetadata values
+                DIRECT_EXECUTOR,
+                mStatsCallbackImpl
+        );
+    }
+
+    int pullUwbActivityInfoLocked(int atomTag, List<StatsEvent> pulledData) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            SynchronousResultReceiver uwbReceiver = new SynchronousResultReceiver("uwb");
+            mUwbManager.getUwbActivityEnergyInfoAsync(Runnable::run,
+                    info -> {
+                        Bundle bundle = new Bundle();
+                        bundle.putParcelable(BatteryStats.RESULT_RECEIVER_CONTROLLER_KEY, info);
+                        uwbReceiver.send(0, bundle);
+                }
+            );
+            final UwbActivityEnergyInfo uwbInfo = awaitControllerInfo(uwbReceiver);
+            if (uwbInfo == null) {
+                return StatsManager.PULL_SKIP;
+            }
+            pulledData.add(
+                    FrameworkStatsLog.buildStatsEvent(atomTag,
+                            uwbInfo.getControllerTxDurationMillis(),
+                            uwbInfo.getControllerRxDurationMillis(),
+                            uwbInfo.getControllerIdleDurationMillis(),
+                            uwbInfo.getControllerWakeCount()));
+        } catch (RuntimeException e) {
+            Slog.e(TAG, "failed to getUwbActivityEnergyInfoAsync", e);
+            return StatsManager.PULL_SKIP;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return StatsManager.PULL_SUCCESS;
+    }
+
     private void registerSystemElapsedRealtime() {
         int tagId = FrameworkStatsLog.SYSTEM_ELAPSED_REALTIME;
         PullAtomMetadata metadata = new PullAtomMetadata.Builder()
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index e178669..51872b3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1229,10 +1229,11 @@
                     return;
                 }
 
+                // Live wallpapers always are system wallpapers unless lock screen live wp is
+                // enabled.
+                which = mIsLockscreenLiveWallpaperEnabled ? mWallpaper.mWhich : FLAG_SYSTEM;
                 mWallpaper.primaryColors = primaryColors;
 
-                // Live wallpapers always are system wallpapers.
-                which = FLAG_SYSTEM;
                 // It's also the lock screen wallpaper when we don't have a bitmap in there.
                 if (displayId == DEFAULT_DISPLAY) {
                     final WallpaperData lockedWallpaper = mLockWallpaperMap.get(mWallpaper.userId);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index be503fc..7d220df 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -82,6 +82,7 @@
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import android.Manifest;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
@@ -261,6 +262,8 @@
     /** Helper for {@link Task#fillTaskInfo}. */
     final TaskInfoHelper mTaskInfoHelper = new TaskInfoHelper();
 
+    final OpaqueActivityHelper mOpaqueActivityHelper = new OpaqueActivityHelper();
+
     private final ActivityTaskSupervisorHandler mHandler;
     final Looper mLooper;
 
@@ -2906,6 +2909,38 @@
         }
     }
 
+    /** The helper to get the top opaque activity of a container. */
+    static class OpaqueActivityHelper implements Predicate<ActivityRecord> {
+        private ActivityRecord mStarting;
+        private boolean mIncludeInvisibleAndFinishing;
+
+        ActivityRecord getOpaqueActivity(@NonNull WindowContainer<?> container) {
+            mIncludeInvisibleAndFinishing = true;
+            return container.getActivity(this,
+                    true /* traverseTopToBottom */, null /* boundary */);
+        }
+
+        ActivityRecord getVisibleOpaqueActivity(@NonNull WindowContainer<?> container,
+                @Nullable ActivityRecord starting) {
+            mStarting = starting;
+            mIncludeInvisibleAndFinishing = false;
+            final ActivityRecord opaque = container.getActivity(this,
+                    true /* traverseTopToBottom */, null /* boundary */);
+            mStarting = null;
+            return opaque;
+        }
+
+        @Override
+        public boolean test(ActivityRecord r) {
+            if (!mIncludeInvisibleAndFinishing && !r.visibleIgnoringKeyguard && r != mStarting) {
+                // Ignore invisible activities that are not the currently starting activity
+                // (about to be visible).
+                return false;
+            }
+            return r.occludesParent(mIncludeInvisibleAndFinishing /* includingFinishing */);
+        }
+    }
+
     /**
      * Fills the info that needs to iterate all activities of task, such as the number of
      * non-finishing activities and collecting launch cookies.
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index d916a1b..7ecc083 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -133,7 +133,7 @@
             return mOrphanTransaction;
         }
 
-        private void onSurfacePlacement() {
+        private void tryFinish() {
             if (!mReady) return;
             ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: onSurfacePlacement checking %s",
                     mSyncId, mRootMembers);
@@ -168,14 +168,13 @@
             class CommitCallback implements Runnable {
                 // Can run a second time if the action completes after the timeout.
                 boolean ran = false;
-                public void onCommitted() {
+                public void onCommitted(SurfaceControl.Transaction t) {
                     synchronized (mWm.mGlobalLock) {
                         if (ran) {
                             return;
                         }
                         mHandler.removeCallbacks(this);
                         ran = true;
-                        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
                         for (WindowContainer wc : wcAwaitingCommit) {
                             wc.onSyncTransactionCommitted(t);
                         }
@@ -194,12 +193,12 @@
                     Slog.e(TAG, "WM sent Transaction to organized, but never received" +
                            " commit callback. Application ANR likely to follow.");
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-                    onCommitted();
-
+                    onCommitted(merged);
                 }
             };
             CommitCallback callback = new CommitCallback();
-            merged.addTransactionCommittedListener((r) -> { r.run(); }, callback::onCommitted);
+            merged.addTransactionCommittedListener(Runnable::run,
+                    () -> callback.onCommitted(new SurfaceControl.Transaction()));
             mHandler.postDelayed(callback, BLAST_TIMEOUT_DURATION);
 
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "onTransactionReady");
@@ -223,6 +222,12 @@
                     }
                 });
             }
+            // Notify idle listeners
+            for (int i = mOnIdleListeners.size() - 1; i >= 0; --i) {
+                // If an idle listener adds a sync, though, then stop notifying.
+                if (mActiveSyncs.size() > 0) break;
+                mOnIdleListeners.get(i).run();
+            }
         }
 
         private void setReady(boolean ready) {
@@ -281,6 +286,8 @@
      */
     private final ArrayList<PendingSyncSet> mPendingSyncSets = new ArrayList<>();
 
+    private final ArrayList<Runnable> mOnIdleListeners = new ArrayList<>();
+
     BLASTSyncEngine(WindowManagerService wms) {
         this(wms, wms.mH);
     }
@@ -379,10 +386,15 @@
     void onSurfacePlacement() {
         // backwards since each state can remove itself if finished
         for (int i = mActiveSyncs.size() - 1; i >= 0; --i) {
-            mActiveSyncs.valueAt(i).onSurfacePlacement();
+            mActiveSyncs.valueAt(i).tryFinish();
         }
     }
 
+    /** Only use this for tests! */
+    void tryFinishForTest(int syncId) {
+        getSyncSet(syncId).tryFinish();
+    }
+
     /**
      * Queues a sync operation onto this engine. It will wait until any current/prior sync-sets
      * have finished to run. This is needed right now because currently {@link BLASTSyncEngine}
@@ -409,4 +421,8 @@
     boolean hasPendingSyncSets() {
         return !mPendingSyncSets.isEmpty();
     }
+
+    void addOnIdleListener(Runnable onIdleListener) {
+        mOnIdleListeners.add(onIdleListener);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index bad64d3..76d6951 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1858,7 +1858,8 @@
             return false;
         }
         if (mLastWallpaperVisible && r.windowsCanBeWallpaperTarget()
-                && mFixedRotationTransitionListener.mAnimatingRecents == null) {
+                && mFixedRotationTransitionListener.mAnimatingRecents == null
+                && !mTransitionController.isTransientLaunch(r)) {
             // Use normal rotation animation for orientation change of visible wallpaper if recents
             // animation is not running (it may be swiping to home).
             return false;
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 3d00686..3551370 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -18,6 +18,7 @@
 
 import static android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
 import static android.view.SurfaceControl.HIDDEN;
+import static android.window.TaskConstants.TASK_CHILD_LAYER_LETTERBOX_BACKGROUND;
 
 import android.content.Context;
 import android.graphics.Color;
@@ -361,7 +362,8 @@
                     .setCallsite("LetterboxSurface.createSurface")
                     .build();
 
-            t.setLayer(mSurface, -1).setColorSpaceAgnostic(mSurface, true);
+            t.setLayer(mSurface, TASK_CHILD_LAYER_LETTERBOX_BACKGROUND)
+                    .setColorSpaceAgnostic(mSurface, true);
         }
 
         void attachInput(WindowState win) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 5c33e64..99d3cc0 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5652,7 +5652,7 @@
                     (deferred) -> {
                         // Need to check again if deferred since the system might
                         // be in a different state.
-                        if (deferred && !canMoveTaskToBack(tr)) {
+                        if (!isAttached() || (deferred && !canMoveTaskToBack(tr))) {
                             Slog.e(TAG, "Failed to move task to back after saying we could: "
                                     + tr.mTaskId);
                             transition.abort();
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 311b9a6..3f7ab14 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -102,8 +102,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.internal.util.function.pooled.PooledLambda;
-import com.android.internal.util.function.pooled.PooledPredicate;
 import com.android.server.am.HostingRecord;
 import com.android.server.pm.pkg.AndroidPackage;
 
@@ -934,11 +932,10 @@
         if (!isAttached() || isForceHidden() || isForceTranslucent()) {
             return true;
         }
-        final PooledPredicate p = PooledLambda.obtainPredicate(TaskFragment::isOpaqueActivity,
-                PooledLambda.__(ActivityRecord.class), starting, false /* including*/);
-        final ActivityRecord opaque = getActivity(p);
-        p.recycle();
-        return opaque == null;
+        // A TaskFragment isn't translucent if it has at least one visible activity that occludes
+        // this TaskFragment.
+        return mTaskSupervisor.mOpaqueActivityHelper.getVisibleOpaqueActivity(this,
+                starting) == null;
     }
 
     /**
@@ -951,25 +948,7 @@
             return true;
         }
         // Including finishing Activity if the TaskFragment is becoming invisible in the transition.
-        final boolean includingFinishing = !isVisibleRequested();
-        final PooledPredicate p = PooledLambda.obtainPredicate(TaskFragment::isOpaqueActivity,
-                PooledLambda.__(ActivityRecord.class), null /* starting */, includingFinishing);
-        final ActivityRecord opaque = getActivity(p);
-        p.recycle();
-        return opaque == null;
-    }
-
-    private static boolean isOpaqueActivity(@NonNull ActivityRecord r,
-            @Nullable ActivityRecord starting, boolean includingFinishing) {
-        if (!r.visibleIgnoringKeyguard && r != starting) {
-            // Also ignore invisible activities that are not the currently starting
-            // activity (about to be visible).
-            return false;
-        }
-
-        // TaskFragment isn't translucent if it has at least one fullscreen activity that is
-        // visible.
-        return r.occludesParent(includingFinishing);
+        return mTaskSupervisor.mOpaqueActivityHelper.getOpaqueActivity(this) == null;
     }
 
     ActivityRecord getTopNonFinishingActivity() {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 76b0e7b..452bd6d 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -104,7 +104,8 @@
 import java.util.function.Predicate;
 
 /**
- * Represents a logical transition.
+ * Represents a logical transition. This keeps track of all the changes associated with a logical
+ * WM state -> state transition.
  * @see TransitionController
  */
 class Transition implements BLASTSyncEngine.TransactionReadyListener {
@@ -416,6 +417,10 @@
         return mFinishTransaction;
     }
 
+    boolean isPending() {
+        return mState == STATE_PENDING;
+    }
+
     boolean isCollecting() {
         return mState == STATE_COLLECTING || mState == STATE_STARTED;
     }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index cbb4fe2..0ae9c4c 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -51,6 +51,7 @@
 import android.window.WindowContainerTransaction;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.ProtoLogGroup;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.server.FgThread;
@@ -88,6 +89,7 @@
 
     private WindowProcessController mTransitionPlayerProc;
     final ActivityTaskManagerService mAtm;
+    BLASTSyncEngine mSyncEngine;
 
     final RemotePlayer mRemotePlayer;
     SnapshotController mSnapshotController;
@@ -121,6 +123,26 @@
 
     private final IBinder.DeathRecipient mTransitionPlayerDeath;
 
+    static class QueuedTransition {
+        final Transition mTransition;
+        final OnStartCollect mOnStartCollect;
+        final BLASTSyncEngine.SyncGroup mLegacySync;
+
+        QueuedTransition(Transition transition, OnStartCollect onStartCollect) {
+            mTransition = transition;
+            mOnStartCollect = onStartCollect;
+            mLegacySync = null;
+        }
+
+        QueuedTransition(BLASTSyncEngine.SyncGroup legacySync, OnStartCollect onStartCollect) {
+            mTransition = null;
+            mOnStartCollect = onStartCollect;
+            mLegacySync = legacySync;
+        }
+    }
+
+    private final ArrayList<QueuedTransition> mQueuedTransitions = new ArrayList<>();
+
     /** The transition currently being constructed (collecting participants). */
     private Transition mCollectingTransition = null;
 
@@ -158,6 +180,14 @@
         mTransitionTracer = wms.mTransitionTracer;
         mIsWaitingForDisplayEnabled = !wms.mDisplayEnabled;
         registerLegacyListener(wms.mActivityManagerAppTransitionNotifier);
+        setSyncEngine(wms.mSyncEngine);
+    }
+
+    @VisibleForTesting
+    void setSyncEngine(BLASTSyncEngine syncEngine) {
+        mSyncEngine = syncEngine;
+        // Check the queue whenever the sync-engine becomes idle.
+        mSyncEngine.addOnIdleListener(this::tryStartCollectFromQueue);
     }
 
     private void detachPlayer() {
@@ -195,7 +225,7 @@
             throw new IllegalStateException("Simultaneous transition collection not supported"
                     + " yet. Use {@link #createPendingTransition} for explicit queueing.");
         }
-        Transition transit = new Transition(type, flags, this, mAtm.mWindowManager.mSyncEngine);
+        Transition transit = new Transition(type, flags, this, mSyncEngine);
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s", transit);
         moveToCollecting(transit);
         return transit;
@@ -325,7 +355,7 @@
     /** @return {@code true} if a transition is running */
     boolean inTransition() {
         // TODO(shell-transitions): eventually properly support multiple
-        return isCollecting() || isPlaying();
+        return isCollecting() || isPlaying() || !mQueuedTransitions.isEmpty();
     }
 
     /** @return {@code true} if a transition is running in a participant subtree of wc */
@@ -453,8 +483,7 @@
             // some frames before and after the display projection transaction is applied by the
             // remote player. That may cause some buffers to show in different rotation. So use
             // sync method to pause clients drawing until the projection transaction is applied.
-            mAtm.mWindowManager.mSyncEngine.setSyncMethod(displayTransition.getSyncId(),
-                    BLASTSyncEngine.METHOD_BLAST);
+            mSyncEngine.setSyncMethod(displayTransition.getSyncId(), BLASTSyncEngine.METHOD_BLAST);
         }
         final Rect startBounds = displayChange.getStartAbsBounds();
         final Rect endBounds = displayChange.getEndAbsBounds();
@@ -741,6 +770,31 @@
         mStateValidators.clear();
     }
 
+    void tryStartCollectFromQueue() {
+        if (mQueuedTransitions.isEmpty()) return;
+        // Only need to try the next one since, even when transition can collect in parallel,
+        // they still need to serialize on readiness.
+        final QueuedTransition queued = mQueuedTransitions.get(0);
+        if (mCollectingTransition != null || mSyncEngine.hasActiveSync()) {
+            return;
+        }
+        mQueuedTransitions.remove(0);
+        // This needs to happen immediately to prevent another sync from claiming the syncset
+        // out-of-order (moveToCollecting calls startSyncSet)
+        if (queued.mTransition != null) {
+            moveToCollecting(queued.mTransition);
+        } else {
+            // legacy sync
+            mSyncEngine.startSyncSet(queued.mLegacySync);
+        }
+        // Post this so that the now-playing transition logic isn't interrupted.
+        mAtm.mH.post(() -> {
+            synchronized (mAtm.mGlobalLock) {
+                queued.mOnStartCollect.onCollectStarted(true /* deferred */);
+            }
+        });
+    }
+
     void moveToPlaying(Transition transition) {
         if (transition != mCollectingTransition) {
             throw new IllegalStateException("Trying to move non-collecting transition to playing");
@@ -749,6 +803,7 @@
         mPlayingTransitions.add(transition);
         updateRunningRemoteAnimation(transition, true /* isPlaying */);
         mTransitionTracer.logState(transition);
+        // Sync engine should become idle after this, so the idle listener will check the queue.
     }
 
     void updateAnimatingState(SurfaceControl.Transaction t) {
@@ -758,12 +813,12 @@
             t.setEarlyWakeupStart();
             // Usually transitions put quite a load onto the system already (with all the things
             // happening in app), so pause task snapshot persisting to not increase the load.
-            mAtm.mWindowManager.mSnapshotController.setPause(true);
+            mSnapshotController.setPause(true);
             mAnimatingState = true;
             Transition.asyncTraceBegin("animating", 0x41bfaf1 /* hashcode of TAG */);
         } else if (!animatingState && mAnimatingState) {
             t.setEarlyWakeupEnd();
-            mAtm.mWindowManager.mSnapshotController.setPause(false);
+            mSnapshotController.setPause(false);
             mAnimatingState = false;
             Transition.asyncTraceEnd(0x41bfaf1 /* hashcode of TAG */);
         }
@@ -793,6 +848,7 @@
         transition.abort();
         mCollectingTransition = null;
         mTransitionTracer.logState(transition);
+        // abort will call through the normal finish paths and thus check the queue.
     }
 
     /**
@@ -874,7 +930,7 @@
         if (!mPlayingTransitions.isEmpty()) {
             state = LEGACY_STATE_RUNNING;
         } else if ((mCollectingTransition != null && mCollectingTransition.getLegacyIsReady())
-                || mAtm.mWindowManager.mSyncEngine.hasPendingSyncSets()) {
+                || mSyncEngine.hasPendingSyncSets()) {
             // The transition may not be "ready", but we have a sync-transaction waiting to start.
             // Usually the pending transaction is for a transition, so assuming that is the case,
             // we can't be IDLE for test purposes. Ideally, we should have a STATE_COLLECTING.
@@ -885,25 +941,43 @@
     }
 
     /** Returns {@code true} if it started collecting, {@code false} if it was queued. */
+    private void queueTransition(Transition transit, OnStartCollect onStartCollect) {
+        mQueuedTransitions.add(new QueuedTransition(transit, onStartCollect));
+        ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+                "Queueing transition: %s", transit);
+    }
+
+    /** Returns {@code true} if it started collecting, {@code false} if it was queued. */
     boolean startCollectOrQueue(Transition transit, OnStartCollect onStartCollect) {
-        if (mAtm.mWindowManager.mSyncEngine.hasActiveSync()) {
+        if (!mQueuedTransitions.isEmpty()) {
+            // Just add to queue since we already have a queue.
+            queueTransition(transit, onStartCollect);
+            return false;
+        }
+        if (mSyncEngine.hasActiveSync()) {
             if (!isCollecting()) {
                 Slog.w(TAG, "Ongoing Sync outside of transition.");
             }
-            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN,
-                    "Queueing transition: %s", transit);
-            mAtm.mWindowManager.mSyncEngine.queueSyncSet(
-                    // Make sure to collect immediately to prevent another transition
-                    // from sneaking in before it. Note: moveToCollecting internally
-                    // calls startSyncSet.
-                    () -> moveToCollecting(transit),
-                    () -> onStartCollect.onCollectStarted(true /* deferred */));
+            queueTransition(transit, onStartCollect);
             return false;
-        } else {
-            moveToCollecting(transit);
-            onStartCollect.onCollectStarted(false /* deferred */);
-            return true;
         }
+        moveToCollecting(transit);
+        onStartCollect.onCollectStarted(false /* deferred */);
+        return true;
+    }
+
+    /** Returns {@code true} if it started collecting, {@code false} if it was queued. */
+    boolean startLegacySyncOrQueue(BLASTSyncEngine.SyncGroup syncGroup, Runnable applySync) {
+        if (!mQueuedTransitions.isEmpty() || mSyncEngine.hasActiveSync()) {
+            // Just add to queue since we already have a queue.
+            mQueuedTransitions.add(new QueuedTransition(syncGroup, (d) -> applySync.run()));
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+                    "Queueing legacy sync-set: %s", syncGroup.mSyncId);
+            return false;
+        }
+        mSyncEngine.startSyncSet(syncGroup);
+        applySync.run();
+        return true;
     }
 
     interface OnStartCollect {
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index a4c931c..6597d4c 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -154,6 +154,7 @@
         }
 
         outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType);
+        outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags());
 
         for (int i = 0; i < targets.size(); ++i) {
             final long changeToken = outputStream
@@ -162,6 +163,7 @@
             final Transition.ChangeInfo target = targets.get(i);
 
             final int mode = target.getTransitMode(target.mContainer);
+            final int flags = target.getChangeFlags(target.mContainer);
             final int layerId;
             if (target.mContainer.mSurfaceControl.isValid()) {
                 layerId = target.mContainer.mSurfaceControl.getLayerId();
@@ -170,6 +172,7 @@
             }
 
             outputStream.write(com.android.server.wm.shell.Target.MODE, mode);
+            outputStream.write(com.android.server.wm.shell.Target.FLAGS, flags);
             outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId);
 
             if (mActiveTracingEnabled) {
diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
index 6c38c6f..1ffee05 100644
--- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java
+++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java
@@ -129,26 +129,10 @@
     }
 
     void updateWallpaperWindows(boolean visible) {
-        boolean changed = false;
         if (mVisibleRequested != visible) {
             ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b",
                     token, visible);
             setVisibility(visible);
-            changed = true;
-        }
-        if (mTransitionController.isShellTransitionsEnabled()) {
-            // Apply legacy fixed rotation to wallpaper if it is becoming visible
-            if (!mTransitionController.useShellTransitionsRotation() && changed && visible) {
-                final WindowState wallpaperTarget =
-                        mDisplayContent.mWallpaperController.getWallpaperTarget();
-                if (wallpaperTarget != null && wallpaperTarget.mToken.hasFixedRotationTransform()) {
-                    linkFixedRotationTransform(wallpaperTarget.mToken);
-                }
-            }
-            // If wallpaper is in transition, setVisible() will be called from commitVisibility()
-            // when finishing transition. Otherwise commitVisibility() is already called from above
-            // setVisibility().
-            return;
         }
 
         final WindowState wallpaperTarget =
@@ -172,6 +156,12 @@
                 linkFixedRotationTransform(wallpaperTarget.mToken);
             }
         }
+        if (mTransitionController.isShellTransitionsEnabled()) {
+            // If wallpaper is in transition, setVisible() will be called from commitVisibility()
+            // when finishing transition. Otherwise commitVisibility() is already called from above
+            // setVisibility().
+            return;
+        }
 
         setVisible(visible);
     }
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ee86b97..cd42528 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -231,19 +231,26 @@
                  */
                 final BLASTSyncEngine.SyncGroup syncGroup = prepareSyncWithOrganizer(callback);
                 final int syncId = syncGroup.mSyncId;
-                if (!mService.mWindowManager.mSyncEngine.hasActiveSync()) {
-                    mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup);
-                    applyTransaction(t, syncId, null /*transition*/, caller);
-                    setSyncReady(syncId);
+                if (mTransitionController.isShellTransitionsEnabled()) {
+                    mTransitionController.startLegacySyncOrQueue(syncGroup, () -> {
+                        applyTransaction(t, syncId, null /*transition*/, caller);
+                        setSyncReady(syncId);
+                    });
                 } else {
-                    // Because the BLAST engine only supports one sync at a time, queue the
-                    // transaction.
-                    mService.mWindowManager.mSyncEngine.queueSyncSet(
-                            () -> mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup),
-                            () -> {
-                                applyTransaction(t, syncId, null /*transition*/, caller);
-                                setSyncReady(syncId);
-                            });
+                    if (!mService.mWindowManager.mSyncEngine.hasActiveSync()) {
+                        mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup);
+                        applyTransaction(t, syncId, null /*transition*/, caller);
+                        setSyncReady(syncId);
+                    } else {
+                        // Because the BLAST engine only supports one sync at a time, queue the
+                        // transaction.
+                        mService.mWindowManager.mSyncEngine.queueSyncSet(
+                                () -> mService.mWindowManager.mSyncEngine.startSyncSet(syncGroup),
+                                () -> {
+                                    applyTransaction(t, syncId, null /*transition*/, caller);
+                                    setSyncReady(syncId);
+                                });
+                    }
                 }
                 return syncId;
             }
diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
index 4898d95..ad098b7 100644
--- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp
+++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp
@@ -233,44 +233,50 @@
 
 // Native methods for VirtualDpad
 static bool nativeWriteDpadKeyEvent(JNIEnv* env, jobject thiz, jlong ptr, jint androidKeyCode,
-                                    jint action) {
+                                    jint action, jlong eventTimeNanos) {
     VirtualDpad* virtualDpad = reinterpret_cast<VirtualDpad*>(ptr);
-    return virtualDpad->writeDpadKeyEvent(androidKeyCode, action);
+    return virtualDpad->writeDpadKeyEvent(androidKeyCode, action,
+                                          std::chrono::nanoseconds(eventTimeNanos));
 }
 
 // Native methods for VirtualKeyboard
 static bool nativeWriteKeyEvent(JNIEnv* env, jobject thiz, jlong ptr, jint androidKeyCode,
-                                jint action) {
+                                jint action, jlong eventTimeNanos) {
     VirtualKeyboard* virtualKeyboard = reinterpret_cast<VirtualKeyboard*>(ptr);
-    return virtualKeyboard->writeKeyEvent(androidKeyCode, action);
+    return virtualKeyboard->writeKeyEvent(androidKeyCode, action,
+                                          std::chrono::nanoseconds(eventTimeNanos));
 }
 
 // Native methods for VirtualTouchscreen
 static bool nativeWriteTouchEvent(JNIEnv* env, jobject thiz, jlong ptr, jint pointerId,
                                   jint toolType, jint action, jfloat locationX, jfloat locationY,
-                                  jfloat pressure, jfloat majorAxisSize) {
+                                  jfloat pressure, jfloat majorAxisSize, jlong eventTimeNanos) {
     VirtualTouchscreen* virtualTouchscreen = reinterpret_cast<VirtualTouchscreen*>(ptr);
     return virtualTouchscreen->writeTouchEvent(pointerId, toolType, action, locationX, locationY,
-                                               pressure, majorAxisSize);
+                                               pressure, majorAxisSize,
+                                               std::chrono::nanoseconds(eventTimeNanos));
 }
 
 // Native methods for VirtualMouse
 static bool nativeWriteButtonEvent(JNIEnv* env, jobject thiz, jlong ptr, jint buttonCode,
-                                   jint action) {
+                                   jint action, jlong eventTimeNanos) {
     VirtualMouse* virtualMouse = reinterpret_cast<VirtualMouse*>(ptr);
-    return virtualMouse->writeButtonEvent(buttonCode, action);
+    return virtualMouse->writeButtonEvent(buttonCode, action,
+                                          std::chrono::nanoseconds(eventTimeNanos));
 }
 
 static bool nativeWriteRelativeEvent(JNIEnv* env, jobject thiz, jlong ptr, jfloat relativeX,
-                                     jfloat relativeY) {
+                                     jfloat relativeY, jlong eventTimeNanos) {
     VirtualMouse* virtualMouse = reinterpret_cast<VirtualMouse*>(ptr);
-    return virtualMouse->writeRelativeEvent(relativeX, relativeY);
+    return virtualMouse->writeRelativeEvent(relativeX, relativeY,
+                                            std::chrono::nanoseconds(eventTimeNanos));
 }
 
 static bool nativeWriteScrollEvent(JNIEnv* env, jobject thiz, jlong ptr, jfloat xAxisMovement,
-                                   jfloat yAxisMovement) {
+                                   jfloat yAxisMovement, jlong eventTimeNanos) {
     VirtualMouse* virtualMouse = reinterpret_cast<VirtualMouse*>(ptr);
-    return virtualMouse->writeScrollEvent(xAxisMovement, yAxisMovement);
+    return virtualMouse->writeScrollEvent(xAxisMovement, yAxisMovement,
+                                          std::chrono::nanoseconds(eventTimeNanos));
 }
 
 static JNINativeMethod methods[] = {
@@ -283,12 +289,12 @@
         {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IILjava/lang/String;II)J",
          (void*)nativeOpenUinputTouchscreen},
         {"nativeCloseUinput", "(J)V", (void*)nativeCloseUinput},
-        {"nativeWriteDpadKeyEvent", "(JII)Z", (void*)nativeWriteDpadKeyEvent},
-        {"nativeWriteKeyEvent", "(JII)Z", (void*)nativeWriteKeyEvent},
-        {"nativeWriteButtonEvent", "(JII)Z", (void*)nativeWriteButtonEvent},
-        {"nativeWriteTouchEvent", "(JIIIFFFF)Z", (void*)nativeWriteTouchEvent},
-        {"nativeWriteRelativeEvent", "(JFF)Z", (void*)nativeWriteRelativeEvent},
-        {"nativeWriteScrollEvent", "(JFF)Z", (void*)nativeWriteScrollEvent},
+        {"nativeWriteDpadKeyEvent", "(JIIJ)Z", (void*)nativeWriteDpadKeyEvent},
+        {"nativeWriteKeyEvent", "(JIIJ)Z", (void*)nativeWriteKeyEvent},
+        {"nativeWriteButtonEvent", "(JIIJ)Z", (void*)nativeWriteButtonEvent},
+        {"nativeWriteTouchEvent", "(JIIIFFFFJ)Z", (void*)nativeWriteTouchEvent},
+        {"nativeWriteRelativeEvent", "(JFFJ)Z", (void*)nativeWriteRelativeEvent},
+        {"nativeWriteScrollEvent", "(JFFJ)Z", (void*)nativeWriteScrollEvent},
 };
 
 int register_android_server_companion_virtual_InputController(JNIEnv* env) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 439ad76..cf57b33 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -529,7 +529,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+            InputReaderConfiguration::Change::DISPLAY_INFO);
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputChannel(
@@ -1079,7 +1079,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+            InputReaderConfiguration::Change::DISPLAY_INFO);
 }
 
 void NativeInputManager::setPointerSpeed(int32_t speed) {
@@ -1095,7 +1095,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_POINTER_SPEED);
+            InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
 void NativeInputManager::setPointerAcceleration(float acceleration) {
@@ -1111,7 +1111,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_POINTER_SPEED);
+            InputReaderConfiguration::Change::POINTER_SPEED);
 }
 
 void NativeInputManager::setTouchpadPointerSpeed(int32_t speed) {
@@ -1127,7 +1127,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_TOUCHPAD_SETTINGS);
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
 void NativeInputManager::setTouchpadNaturalScrollingEnabled(bool enabled) {
@@ -1143,7 +1143,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_TOUCHPAD_SETTINGS);
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
 void NativeInputManager::setTouchpadTapToClickEnabled(bool enabled) {
@@ -1159,7 +1159,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_TOUCHPAD_SETTINGS);
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
 void NativeInputManager::setTouchpadRightClickZoneEnabled(bool enabled) {
@@ -1175,7 +1175,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_TOUCHPAD_SETTINGS);
+            InputReaderConfiguration::Change::TOUCHPAD_SETTINGS);
 }
 
 void NativeInputManager::setInputDeviceEnabled(uint32_t deviceId, bool enabled) {
@@ -1193,7 +1193,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_ENABLED_STATE);
+            InputReaderConfiguration::Change::ENABLED_STATE);
 }
 
 void NativeInputManager::setShowTouches(bool enabled) {
@@ -1209,7 +1209,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_SHOW_TOUCHES);
+            InputReaderConfiguration::Change::SHOW_TOUCHES);
 }
 
 void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, bool enabled) {
@@ -1222,7 +1222,7 @@
 
 void NativeInputManager::reloadCalibration() {
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_TOUCH_AFFINE_TRANSFORMATION);
+            InputReaderConfiguration::Change::TOUCH_AFFINE_TRANSFORMATION);
 }
 
 void NativeInputManager::setPointerIconType(PointerIconStyle iconId) {
@@ -1516,7 +1516,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_POINTER_CAPTURE);
+            InputReaderConfiguration::Change::POINTER_CAPTURE);
 }
 
 void NativeInputManager::loadPointerIcon(SpriteIcon* icon, int32_t displayId) {
@@ -1626,7 +1626,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_STYLUS_BUTTON_REPORTING);
+            InputReaderConfiguration::Change::STYLUS_BUTTON_REPORTING);
 }
 
 FloatPoint NativeInputManager::getMouseCursorPosition() {
@@ -1649,7 +1649,7 @@
     } // release lock
 
     mInputManager->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+            InputReaderConfiguration::Change::DISPLAY_INFO);
 }
 
 // ----------------------------------------------------------------------------
@@ -2300,14 +2300,14 @@
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS);
+            InputReaderConfiguration::Change::KEYBOARD_LAYOUTS);
 }
 
 static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DEVICE_ALIAS);
+            InputReaderConfiguration::Change::DEVICE_ALIAS);
 }
 
 static void nativeSysfsNodeChanged(JNIEnv* env, jobject nativeImplObj, jstring path) {
@@ -2403,7 +2403,7 @@
 static void nativeNotifyPortAssociationsChanged(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+            InputReaderConfiguration::Change::DISPLAY_INFO);
 }
 
 static void nativeSetDisplayEligibilityForPointerCapture(JNIEnv* env, jobject nativeImplObj,
@@ -2416,19 +2416,19 @@
 static void nativeChangeUniqueIdAssociation(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DISPLAY_INFO);
+            InputReaderConfiguration::Change::DISPLAY_INFO);
 }
 
 static void nativeChangeTypeAssociation(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_DEVICE_TYPE);
+            InputReaderConfiguration::Change::DEVICE_TYPE);
 }
 
 static void changeKeyboardLayoutAssociation(JNIEnv* env, jobject nativeImplObj) {
     NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
-            InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUT_ASSOCIATION);
+            InputReaderConfiguration::Change::KEYBOARD_LAYOUT_ASSOCIATION);
 }
 
 static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index e256148..47b45ac 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -43,7 +43,9 @@
     public static final String USER_CANCELED_SUBSTRING = "TYPE_USER_CANCELED";
 
     public static final int DEFAULT_INT_32 = -1;
+    public static final String DEFAULT_STRING = "";
     public static final int[] DEFAULT_REPEATED_INT_32 = new int[0];
+    public static final String[] DEFAULT_REPEATED_STR = new String[0];
     // Used for single count metric emits, such as singular amounts of various types
     public static final int UNIT = 1;
     // Used for zero count metric emits, such as zero amounts of various types
@@ -143,7 +145,12 @@
                     finalPhaseMetric.getAuthenticationEntryCount(),
                     /* clicked_entries */ browsedClickedEntries,
                     /* provider_of_clicked_entry */ browsedProviderUid,
-                    /* api_status */ apiStatus
+                    /* api_status */ apiStatus,
+                    DEFAULT_REPEATED_INT_32,
+                    DEFAULT_REPEATED_INT_32,
+                    DEFAULT_REPEATED_STR,
+                    DEFAULT_REPEATED_INT_32,
+                    DEFAULT_STRING
             );
         } catch (Exception e) {
             Log.w(TAG, "Unexpected error during metric logging: " + e);
@@ -222,7 +229,11 @@
                     /* candidate_provider_credential_entry_type_count */
                     candidateCredentialTypeCountList,
                     /* candidate_provider_remote_entry_count */ candidateRemoteEntryCountList,
-                    /* candidate_provider_authentication_entry_count */ candidateAuthEntryCountList
+                    /* candidate_provider_authentication_entry_count */ candidateAuthEntryCountList,
+                    DEFAULT_REPEATED_STR,
+                    false,
+                    DEFAULT_REPEATED_STR,
+                    DEFAULT_REPEATED_INT_32
             );
         } catch (Exception e) {
             Log.w(TAG, "Unexpected error during metric logging: " + e);
@@ -285,10 +296,12 @@
                     /* initial_timestamp_reference_nanoseconds */
                     initialPhaseMetric.getCredentialServiceStartedTimeNanoseconds(),
                     /* count_credential_request_classtypes */
-                    initialPhaseMetric.getCountRequestClassType()
+                    initialPhaseMetric.getCountRequestClassType(),
                     // TODO(b/271135048) - add total count of request options
                     // TODO(b/271135048) - Uncomment once built past PWG review -
-                    // initialPhaseMetric.isOriginSpecified()
+                    DEFAULT_REPEATED_STR,
+                    DEFAULT_REPEATED_INT_32,
+                    initialPhaseMetric.isOriginSpecified()
             );
         } catch (Exception e) {
             Log.w(TAG, "Unexpected error during metric logging: " + e);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 7e5d5aa..4d739d2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -160,6 +160,7 @@
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT;
 import static android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED;
 import static android.app.admin.DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED;
 import static android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_EXPLICITLY;
@@ -533,7 +534,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
@@ -1186,9 +1186,9 @@
                         // Resume logging if all remaining users are affiliated.
                         maybeResumeDeviceWideLoggingLocked();
                     }
-                    if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) {
-                        mDevicePolicyEngine.handleUserRemoved(userHandle);
-                    }
+                }
+                if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) {
+                    mDevicePolicyEngine.handleUserRemoved(userHandle);
                 }
             } else if (Intent.ACTION_USER_STARTED.equals(action)) {
                 sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STARTED, userHandle);
@@ -4157,8 +4157,9 @@
         checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_REMOVE_ACTIVE_ADMIN);
         enforceUserUnlocked(userHandle);
 
+        ActiveAdmin admin;
         synchronized (getLockObject()) {
-            ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
+            admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle);
             if (admin == null) {
                 return;
             }
@@ -4169,14 +4170,13 @@
                         + adminReceiver);
                 return;
             }
-
             mInjector.binderWithCleanCallingIdentity(() ->
                     removeActiveAdminLocked(adminReceiver, userHandle));
-            if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) {
-                mDevicePolicyEngine.removePoliciesForAdmin(
-                        EnforcingAdmin.createEnterpriseEnforcingAdmin(
-                                adminReceiver, userHandle, admin));
-            }
+        }
+        if (isPolicyEngineForFinanceFlagEnabled() || isPermissionCheckFlagEnabled()) {
+            mDevicePolicyEngine.removePoliciesForAdmin(
+                    EnforcingAdmin.createEnterpriseEnforcingAdmin(
+                            adminReceiver, userHandle, admin));
         }
     }
 
@@ -13804,8 +13804,6 @@
                         admin,
                         new BooleanPolicyValue(hidden),
                         userId);
-                Boolean resolvedPolicy = mDevicePolicyEngine.getResolvedPolicy(
-                        PolicyDefinition.APPLICATION_HIDDEN(packageName), userId);
                 result = mInjector.binderWithCleanCallingIdentity(() -> {
                     try {
                         // This is a best effort to continue returning the same value that was
@@ -16549,11 +16547,13 @@
                 hasCallingOrSelfPermission(permission.NOTIFY_PENDING_SYSTEM_UPDATE),
                 "Only the system update service can broadcast update information");
 
-        if (UserHandle.getCallingUserId() != UserHandle.USER_SYSTEM) {
-            Slogf.w(LOG_TAG, "Only the system update service in the system user can broadcast "
-                    + "update information.");
-            return;
-        }
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            if (!mUserManager.getUserInfo(UserHandle.getCallingUserId()).isMain()) {
+                Slogf.w(LOG_TAG, "Only the system update service in the main user can broadcast "
+                        + "update information.");
+                return;
+            }
+        });
 
         if (!mOwners.saveSystemUpdateInfo(info)) {
             // Pending system update hasn't changed, don't send duplicate notification.
@@ -16661,8 +16661,9 @@
                 enforcePermissionGrantStateOnFinancedDevice(packageName, permission);
             }
         }
+        EnforcingAdmin enforcingAdmin;
         if (isPermissionCheckFlagEnabled()) {
-            EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
+            enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
                     admin,
                     MANAGE_DEVICE_POLICY_RUNTIME_PERMISSIONS,
                     callerPackage,
@@ -16686,17 +16687,6 @@
                 callback.sendResult(null);
                 return;
             }
-            // TODO(b/266924257): decide how to handle the internal state if the package doesn't
-            //  exist, or the permission isn't requested by the app, because we could end up with
-            //  inconsistent state between the policy engine and package manager. Also a package
-            //  might get removed or has it's permission updated after we've set the policy.
-            mDevicePolicyEngine.setLocalPolicy(
-                    PolicyDefinition.PERMISSION_GRANT(packageName, permission),
-                    enforcingAdmin,
-                    new IntegerPolicyValue(grantState),
-                    caller.getUserId());
-            // TODO: update javadoc to reflect that callback no longer return success/failure
-            callback.sendResult(Bundle.EMPTY);
         } else {
             Preconditions.checkCallAuthorization((caller.hasAdminComponent()
                     && (isProfileOwner(caller) || isDefaultDeviceOwner(caller)
@@ -16704,51 +16694,81 @@
                     || (caller.hasPackage() && isCallerDelegate(caller,
                     DELEGATION_PERMISSION_GRANT)));
             synchronized (getLockObject()) {
-            long ident = mInjector.binderClearCallingIdentity();
-            try {
-                boolean isPostQAdmin = getTargetSdk(caller.getPackageName(), caller.getUserId())
-                        >= android.os.Build.VERSION_CODES.Q;
-                if (!isPostQAdmin) {
-                    // Legacy admins assume that they cannot control pre-M apps
-                    if (getTargetSdk(packageName, caller.getUserId())
-                            < android.os.Build.VERSION_CODES.M) {
+                long ident = mInjector.binderClearCallingIdentity();
+                try {
+                    boolean isPostQAdmin = getTargetSdk(caller.getPackageName(), caller.getUserId())
+                            >= android.os.Build.VERSION_CODES.Q;
+                    if (!isPostQAdmin) {
+                        // Legacy admins assume that they cannot control pre-M apps
+                        if (getTargetSdk(packageName, caller.getUserId())
+                                < android.os.Build.VERSION_CODES.M) {
+                            callback.sendResult(null);
+                            return;
+                        }
+                    }
+                    if (!isRuntimePermission(permission)) {
                         callback.sendResult(null);
                         return;
                     }
-                }
-                if (!isRuntimePermission(permission)) {
+                } catch (SecurityException e) {
+                    Slogf.e(LOG_TAG, "Could not set permission grant state", e);
                     callback.sendResult(null);
-                    return;
+                } finally {
+                    mInjector.binderRestoreCallingIdentity(ident);
                 }
-                if (grantState == PERMISSION_GRANT_STATE_GRANTED
-                        || grantState == DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
-                        || grantState == DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT) {
-                    AdminPermissionControlParams permissionParams =
-                            new AdminPermissionControlParams(packageName, permission,
-                                    grantState,
-                                    canAdminGrantSensorsPermissions());
-                    mInjector.getPermissionControllerManager(caller.getUserHandle())
-                            .setRuntimePermissionGrantStateByDeviceAdmin(
-                                    caller.getPackageName(),
-                                    permissionParams, mContext.getMainExecutor(),
-                                    (permissionWasSet) -> {
-                                        if (isPostQAdmin && !permissionWasSet) {
-                                            callback.sendResult(null);
-                                            return;
-                                        }
-
-                                        DevicePolicyEventLogger
-                                                .createEvent(DevicePolicyEnums
-                                                        .SET_PERMISSION_GRANT_STATE)
-                                                .setAdmin(caller.getPackageName())
-                                                .setStrings(permission)
-                                                .setInt(grantState)
-                                                .setBoolean(
-                                                        /* isDelegate */ isCallerDelegate(caller))
-                                                .write();
-
-                                        callback.sendResult(Bundle.EMPTY);
-                                    });
+            }
+        }
+        // TODO(b/278710449): enable when we stop policy enforecer callback from blocking the main
+        //  thread
+        if (false) {
+            // TODO(b/266924257): decide how to handle the internal state if the package doesn't
+            //  exist, or the permission isn't requested by the app, because we could end up with
+            //  inconsistent state between the policy engine and package manager. Also a package
+            //  might get removed or has it's permission updated after we've set the policy.
+            if (grantState == PERMISSION_GRANT_STATE_DEFAULT) {
+                mDevicePolicyEngine.removeLocalPolicy(
+                        PolicyDefinition.PERMISSION_GRANT(packageName, permission),
+                        enforcingAdmin,
+                        caller.getUserId());
+            } else {
+                mDevicePolicyEngine.setLocalPolicy(
+                        PolicyDefinition.PERMISSION_GRANT(packageName, permission),
+                        enforcingAdmin,
+                        new IntegerPolicyValue(grantState),
+                        caller.getUserId());
+            }
+            int newState = mInjector.binderWithCleanCallingIdentity(() ->
+                    getPermissionGrantStateForUser(
+                            packageName, permission, caller, caller.getUserId()));
+            if (newState == grantState) {
+                callback.sendResult(Bundle.EMPTY);
+            } else {
+                callback.sendResult(null);
+            }
+        } else {
+            synchronized (getLockObject()) {
+                long ident = mInjector.binderClearCallingIdentity();
+                try {
+                    boolean isPostQAdmin = getTargetSdk(caller.getPackageName(), caller.getUserId())
+                            >= android.os.Build.VERSION_CODES.Q;
+                    if (grantState == PERMISSION_GRANT_STATE_GRANTED
+                            || grantState == DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
+                            || grantState == DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT) {
+                        AdminPermissionControlParams permissionParams =
+                                new AdminPermissionControlParams(packageName, permission,
+                                        grantState,
+                                        canAdminGrantSensorsPermissions());
+                        mInjector.getPermissionControllerManager(caller.getUserHandle())
+                                .setRuntimePermissionGrantStateByDeviceAdmin(
+                                        caller.getPackageName(),
+                                        permissionParams, mContext.getMainExecutor(),
+                                        (permissionWasSet) -> {
+                                            if (isPostQAdmin && !permissionWasSet) {
+                                                callback.sendResult(null);
+                                                return;
+                                            }
+                                            callback.sendResult(Bundle.EMPTY);
+                                        });
                     }
                 } catch (SecurityException e) {
                     Slogf.e(LOG_TAG, "Could not set permission grant state", e);
@@ -16759,6 +16779,12 @@
                 }
             }
         }
+        DevicePolicyEventLogger.createEvent(DevicePolicyEnums.SET_PERMISSION_GRANT_STATE)
+                .setAdmin(caller.getPackageName())
+                .setStrings(permission)
+                .setInt(grantState)
+                .setBoolean(/* isDelegate */ isCallerDelegate(caller))
+                .write();
     }
 
     private static final List<String> SENSOR_PERMISSIONS = new ArrayList<>();
@@ -16822,10 +16848,8 @@
             if (isFinancedDeviceOwner(caller)) {
                 enforcePermissionGrantStateOnFinancedDevice(packageName, permission);
             }
-            return mInjector.binderWithCleanCallingIdentity(() -> {
-                return getPermissionGrantStateForUser(
-                        packageName, permission, caller, caller.getUserId());
-            });
+            return mInjector.binderWithCleanCallingIdentity(() -> getPermissionGrantStateForUser(
+                    packageName, permission, caller, caller.getUserId()));
         }
     }
 
@@ -18952,7 +18976,7 @@
                     admin,
                     MANAGE_DEVICE_POLICY_RESET_PASSWORD,
                     caller.getPackageName(),
-                    UserHandle.USER_ALL);
+                    userId);
             Long currentTokenHandle = mDevicePolicyEngine.getLocalPolicySetByAdmin(
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
@@ -19016,7 +19040,7 @@
                     admin,
                     MANAGE_DEVICE_POLICY_RESET_PASSWORD,
                     caller.getPackageName(),
-                    UserHandle.USER_ALL);
+                    userId);
             Long currentTokenHandle = mDevicePolicyEngine.getLocalPolicySetByAdmin(
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
@@ -19062,7 +19086,7 @@
                     admin,
                     MANAGE_DEVICE_POLICY_RESET_PASSWORD,
                     caller.getPackageName(),
-                    UserHandle.USER_ALL);
+                    userId);
             Long currentTokenHandle = mDevicePolicyEngine.getLocalPolicySetByAdmin(
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
@@ -19114,7 +19138,7 @@
                     admin,
                     MANAGE_DEVICE_POLICY_RESET_PASSWORD,
                     caller.getPackageName(),
-                    UserHandle.USER_ALL);
+                    userId);
             Long currentTokenHandle = mDevicePolicyEngine.getLocalPolicySetByAdmin(
                     PolicyDefinition.RESET_PASSWORD_TOKEN,
                     enforcingAdmin,
@@ -19140,10 +19164,17 @@
         }
 
         if (result) {
-            DevicePolicyEventLogger
-                    .createEvent(DevicePolicyEnums.RESET_PASSWORD_WITH_TOKEN)
-                    .setAdmin(caller.getComponentName())
-                    .write();
+            if (isPermissionCheckFlagEnabled()) {
+                DevicePolicyEventLogger
+                        .createEvent(DevicePolicyEnums.RESET_PASSWORD_WITH_TOKEN)
+                        .setAdmin(callerPackageName)
+                        .write();
+            } else {
+                DevicePolicyEventLogger
+                        .createEvent(DevicePolicyEnums.RESET_PASSWORD_WITH_TOKEN)
+                        .setAdmin(caller.getComponentName())
+                        .write();
+            }
         }
         return result;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index d65d366..12a8a75 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -84,6 +84,7 @@
                     ? DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
                     : grantState;
 
+            // TODO(b/278710449): stop blocking in the main thread
             BlockingCallback callback = new BlockingCallback();
             // TODO: remove canAdminGrantSensorPermissions once we expose a new method in
             //  permissionController that doesn't need it.
diff --git a/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
new file mode 100644
index 0000000..77c3396
--- /dev/null
+++ b/services/tests/RemoteProvisioningServiceTests/src/com/android/server/security/rkp/RemoteProvisioningShellCommandTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.security.rkp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.security.keymint.DeviceInfo;
+import android.hardware.security.keymint.IRemotelyProvisionedComponent;
+import android.hardware.security.keymint.MacedPublicKey;
+import android.hardware.security.keymint.ProtectedData;
+import android.hardware.security.keymint.RpcHardwareInfo;
+import android.os.Binder;
+import android.os.FileUtils;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class RemoteProvisioningShellCommandTest {
+
+    private static class Injector extends RemoteProvisioningShellCommand.Injector {
+
+        private final Map<String, IRemotelyProvisionedComponent> mIrpcs;
+
+        Injector(Map irpcs) {
+            mIrpcs = irpcs;
+        }
+
+        @Override
+        String[] getIrpcNames() {
+            return mIrpcs.keySet().toArray(new String[0]);
+        }
+
+        @Override
+        IRemotelyProvisionedComponent getIrpcBinder(String name) {
+            IRemotelyProvisionedComponent irpc = mIrpcs.get(name);
+            if (irpc == null) {
+                throw new IllegalArgumentException("failed to find " + irpc);
+            }
+            return irpc;
+        }
+    }
+
+    private static class CommandResult {
+        private int mCode;
+        private String mOut;
+        private String mErr;
+
+        CommandResult(int code, String out, String err) {
+            mCode = code;
+            mOut = out;
+            mErr = err;
+        }
+
+        int getCode() {
+            return mCode;
+        }
+
+        String getOut() {
+            return mOut;
+        }
+
+        String getErr() {
+            return mErr;
+        }
+    }
+
+    private static CommandResult exec(
+            RemoteProvisioningShellCommand cmd, String[] args) throws Exception {
+        File in = File.createTempFile("rpsct_in_", null);
+        File out = File.createTempFile("rpsct_out_", null);
+        File err = File.createTempFile("rpsct_err_", null);
+        int code = cmd.exec(
+                new Binder(),
+                new FileInputStream(in).getFD(),
+                new FileOutputStream(out).getFD(),
+                new FileOutputStream(err).getFD(),
+                args);
+        return new CommandResult(
+                code, FileUtils.readTextFile(out, 0, null), FileUtils.readTextFile(err, 0, null));
+    }
+
+    @Test
+    public void list_zeroInstances() throws Exception {
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of()));
+        CommandResult res = exec(cmd, new String[] {"list"});
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+        assertThat(res.getOut()).isEmpty();
+    }
+
+    @Test
+    public void list_oneInstances() throws Exception {
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of("default", mock(IRemotelyProvisionedComponent.class))));
+        CommandResult res = exec(cmd, new String[] {"list"});
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+        assertThat(Arrays.asList(res.getOut().split("\n"))).containsExactly("default");
+    }
+
+    @Test
+    public void list_twoInstances() throws Exception {
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of(
+                       "default", mock(IRemotelyProvisionedComponent.class),
+                       "strongbox", mock(IRemotelyProvisionedComponent.class))));
+        CommandResult res = exec(cmd, new String[] {"list"});
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+        assertThat(Arrays.asList(res.getOut().split("\n"))).containsExactly("default", "strongbox");
+    }
+
+    @Test
+    public void csr_hwVersion1_withChallenge() throws Exception {
+        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
+        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
+        defaultInfo.versionNumber = 1;
+        defaultInfo.supportedEekCurve = RpcHardwareInfo.CURVE_25519;
+        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
+        doAnswer(invocation -> {
+            ((DeviceInfo) invocation.getArgument(4)).deviceInfo = new byte[] {0x00};
+            ((ProtectedData) invocation.getArgument(5)).protectedData = new byte[] {0x00};
+            return new byte[] {0x77, 0x77, 0x77, 0x77};
+        }).when(defaultMock).generateCertificateRequest(
+                anyBoolean(), any(), any(), any(), any(), any());
+
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of("default", defaultMock)));
+        CommandResult res = exec(cmd, new String[] {
+                "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"});
+        verify(defaultMock).generateCertificateRequest(
+                /*test_mode=*/eq(false),
+                eq(new MacedPublicKey[0]),
+                eq(Base64.getDecoder().decode(RemoteProvisioningShellCommand.EEK_ED25519_BASE64)),
+                eq(new byte[] {
+                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74,
+                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74}),
+                any(DeviceInfo.class),
+                any(ProtectedData.class));
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+    }
+
+    @Test
+    public void csr_hwVersion2_withChallenge() throws Exception {
+        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
+        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
+        defaultInfo.versionNumber = 2;
+        defaultInfo.supportedEekCurve = RpcHardwareInfo.CURVE_P256;
+        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
+        doAnswer(invocation -> {
+            ((DeviceInfo) invocation.getArgument(4)).deviceInfo = new byte[] {0x00};
+            ((ProtectedData) invocation.getArgument(5)).protectedData = new byte[] {0x00};
+            return new byte[] {0x77, 0x77, 0x77, 0x77};
+        }).when(defaultMock).generateCertificateRequest(
+                anyBoolean(), any(), any(), any(), any(), any());
+
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of("default", defaultMock)));
+        CommandResult res = exec(cmd, new String[] {
+                "csr", "--challenge", "dGVzdHRlc3R0ZXN0dGVzdA==", "default"});
+        verify(defaultMock).generateCertificateRequest(
+                /*test_mode=*/eq(false),
+                eq(new MacedPublicKey[0]),
+                eq(Base64.getDecoder().decode(RemoteProvisioningShellCommand.EEK_P256_BASE64)),
+                eq(new byte[] {
+                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74,
+                        0x74, 0x65, 0x73, 0x74, 0x74, 0x65, 0x73, 0x74}),
+                any(DeviceInfo.class),
+                any(ProtectedData.class));
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+    }
+
+    @Test
+    public void csr_hwVersion3_withoutChallenge() throws Exception {
+        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
+        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
+        defaultInfo.versionNumber = 3;
+        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
+        when(defaultMock.generateCertificateRequestV2(any(), any()))
+            .thenReturn(new byte[] {0x68, 0x65, 0x6c, 0x6c, 0x6f});
+
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of("default", defaultMock)));
+        CommandResult res = exec(cmd, new String[] {"csr", "default"});
+        verify(defaultMock).generateCertificateRequestV2(new MacedPublicKey[0], new byte[0]);
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+        assertThat(res.getOut()).isEqualTo("aGVsbG8=\n");
+    }
+
+    @Test
+    public void csr_hwVersion3_withChallenge() throws Exception {
+        IRemotelyProvisionedComponent defaultMock = mock(IRemotelyProvisionedComponent.class);
+        RpcHardwareInfo defaultInfo = new RpcHardwareInfo();
+        defaultInfo.versionNumber = 3;
+        when(defaultMock.getHardwareInfo()).thenReturn(defaultInfo);
+        when(defaultMock.generateCertificateRequestV2(any(), any()))
+            .thenReturn(new byte[] {0x68, 0x69});
+
+        RemoteProvisioningShellCommand cmd = new RemoteProvisioningShellCommand(
+                new Injector(Map.of("default", defaultMock)));
+        CommandResult res = exec(cmd, new String[] {"csr", "--challenge", "dHJpYWw=", "default"});
+        verify(defaultMock).generateCertificateRequestV2(
+                new MacedPublicKey[0], new byte[] {0x74, 0x72, 0x69, 0x61, 0x6c});
+        assertThat(res.getErr()).isEmpty();
+        assertThat(res.getCode()).isEqualTo(0);
+        assertThat(res.getOut()).isEqualTo("aGk=\n");
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index ca857f1..c4aa0bb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -445,7 +445,7 @@
     }
 
     @Test
-    public void testDisplayBrightnessFollowersRemoval() {
+    public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
         DisplayPowerControllerHolder followerDpc = createDisplayPowerController(FOLLOWER_DISPLAY_ID,
                 FOLLOWER_UNIQUE_ID);
         DisplayPowerControllerHolder secondFollowerDpc = createDisplayPowerController(
@@ -520,6 +520,78 @@
     }
 
     @Test
+    public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
+        DisplayPowerControllerHolder followerHolder =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        DisplayPowerControllerHolder secondFollowerHolder =
+                createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
+                        SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        // Set the initial brightness on the DPCs we're going to remove so we have a fixed value for
+        // it to return to.
+        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
+        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(secondFollowerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener secondFollowerListener =
+                listenerCaptor.getValue();
+        final float initialFollowerBrightness = 0.3f;
+        when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
+                initialFollowerBrightness);
+        when(secondFollowerHolder.brightnessSetting.getBrightness()).thenReturn(
+                initialFollowerBrightness);
+        followerListener.onBrightnessChanged(initialFollowerBrightness);
+        secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
+        advanceTime(1);
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+
+        mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
+        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
+        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
+
+        // Validate both followers are correctly registered and receiving brightness updates
+        float brightness = 0.6f;
+        float nits = 600;
+        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+        clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
+
+        // Stop the lead DPC and validate that the followers go back to their original brightness.
+        mHolder.dpc.stop();
+        advanceTime(1);
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
+    }
+
+    @Test
     public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
         // We should still set screen state for the default display
         DisplayPowerRequest dpr = new DisplayPowerRequest();
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
index 0b97c5c..415adbb 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -91,7 +91,7 @@
     private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
     private static final String UNIQUE_ID = "unique_id_test123";
     private static final int FOLLOWER_DISPLAY_ID = DISPLAY_ID + 1;
-    private static final String FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_456";
+    private static final String FOLLOWER_UNIQUE_ID = "unique_id_456";
     private static final int SECOND_FOLLOWER_DISPLAY_ID = FOLLOWER_DISPLAY_ID + 1;
     private static final String SECOND_FOLLOWER_UNIQUE_DISPLAY_ID = "unique_id_789";
     private static final float PROX_SENSOR_MAX_RANGE = 5;
@@ -279,7 +279,7 @@
     @Test
     public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
         DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
 
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
@@ -298,7 +298,7 @@
     @Test
     public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
         DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -344,7 +344,7 @@
     @Test
     public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -372,7 +372,7 @@
     @Test
     public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
         DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -398,7 +398,7 @@
     @Test
     public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
         DisplayPowerControllerHolder followerDpc =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
 
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
@@ -449,9 +449,9 @@
     }
 
     @Test
-    public void testDisplayBrightnessFollowersRemoval() {
+    public void testDisplayBrightnessFollowersRemoval_RemoveSingleFollower() {
         DisplayPowerControllerHolder followerHolder =
-                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_DISPLAY_ID);
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
         DisplayPowerControllerHolder secondFollowerHolder =
                 createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
                         SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
@@ -525,6 +525,78 @@
     }
 
     @Test
+    public void testDisplayBrightnessFollowersRemoval_RemoveAllFollowers() {
+        DisplayPowerControllerHolder followerHolder =
+                createDisplayPowerController(FOLLOWER_DISPLAY_ID, FOLLOWER_UNIQUE_ID);
+        DisplayPowerControllerHolder secondFollowerHolder =
+                createDisplayPowerController(SECOND_FOLLOWER_DISPLAY_ID,
+                        SECOND_FOLLOWER_UNIQUE_DISPLAY_ID);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        followerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        secondFollowerHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        // Set the initial brightness on the DPCs we're going to remove so we have a fixed value for
+        // it to return to.
+        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(followerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener followerListener = listenerCaptor.getValue();
+        listenerCaptor = ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(secondFollowerHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener secondFollowerListener =
+                listenerCaptor.getValue();
+        final float initialFollowerBrightness = 0.3f;
+        when(followerHolder.brightnessSetting.getBrightness()).thenReturn(
+                initialFollowerBrightness);
+        when(secondFollowerHolder.brightnessSetting.getBrightness()).thenReturn(
+                initialFollowerBrightness);
+        followerListener.onBrightnessChanged(initialFollowerBrightness);
+        secondFollowerListener.onBrightnessChanged(initialFollowerBrightness);
+        advanceTime(1);
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+
+        mHolder.dpc.addDisplayBrightnessFollower(followerHolder.dpc);
+        mHolder.dpc.addDisplayBrightnessFollower(secondFollowerHolder.dpc);
+        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
+
+        // Validate both followers are correctly registered and receiving brightness updates
+        float brightness = 0.6f;
+        float nits = 600;
+        when(mHolder.automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+        when(followerHolder.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(secondFollowerHolder.automaticBrightnessController.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(followerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+        clearInvocations(mHolder.animator, followerHolder.animator, secondFollowerHolder.animator);
+
+        // Stop the lead DPC and validate that the followers go back to their original brightness.
+        mHolder.dpc.stop();
+        advanceTime(1);
+        verify(followerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        verify(secondFollowerHolder.animator).animateTo(eq(initialFollowerBrightness),
+                anyFloat(), anyFloat());
+        clearInvocations(followerHolder.animator, secondFollowerHolder.animator);
+    }
+
+    @Test
     public void testDoesNotSetScreenStateForNonDefaultDisplayUntilBootCompleted() {
         // We should still set screen state for the default display
         DisplayPowerRequest dpr = new DisplayPowerRequest();
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/OWNERS b/services/tests/mockingservicestests/src/com/android/server/rollback/OWNERS
new file mode 100644
index 0000000..daa0211
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/OWNERS
@@ -0,0 +1,3 @@
+ancr@google.com
+harshitmahajan@google.com
+robertogil@google.com
diff --git a/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
index a140730..35d4ffd 100644
--- a/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/rollback/RollbackPackageHealthObserverTest.java
@@ -145,7 +145,7 @@
                 observer.onHealthCheckFailed(null,
                         PackageWatchdog.FAILURE_REASON_NATIVE_CRASH, 1));
         // non-native crash for the package
-        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
+        assertEquals(PackageWatchdog.PackageHealthObserverImpact.USER_IMPACT_LEVEL_60,
                 observer.onHealthCheckFailed(testFailedPackage,
                         PackageWatchdog.FAILURE_REASON_APP_CRASH, 1));
         // non-native crash for a different package
diff --git a/services/tests/servicestests/res/xml/irq_device_map_3.xml b/services/tests/servicestests/res/xml/irq_device_map_3.xml
index 7e2529a..fd55428 100644
--- a/services/tests/servicestests/res/xml/irq_device_map_3.xml
+++ b/services/tests/servicestests/res/xml/irq_device_map_3.xml
@@ -26,4 +26,10 @@
     <device name="test.sound_trigger.device">
         <subsystem>Sound_trigger</subsystem>
     </device>
+    <device name="test.cellular_data.device">
+        <subsystem>Cellular_data</subsystem>
+    </device>
+    <device name="test.sensor.device">
+        <subsystem>Sensor</subsystem>
+    </device>
 </irq-device-map>
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index f1ad577..a01c7bd 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -200,7 +200,7 @@
         assertFalse(mFullScreenMagnificationController.isRegistered(DISPLAY_0));
         assertFalse(mFullScreenMagnificationController.isRegistered(DISPLAY_1));
 
-        verify(mMockThumbnail, times(2)).hideThumbNail();
+        verify(mMockThumbnail, times(2)).hideThumbnail();
     }
 
     @Test
@@ -538,7 +538,10 @@
                 mConfigCaptor.capture());
         assertConfigEquals(config, mConfigCaptor.getValue());
 
-        verify(mMockThumbnail).setThumbNailBounds(any(), anyFloat(), anyFloat(), anyFloat());
+        // The first time is triggered when the thumbnail is just created.
+        // The second time is triggered when the magnification region changed.
+        verify(mMockThumbnail, times(2)).setThumbnailBounds(
+                any(), anyFloat(), anyFloat(), anyFloat());
     }
 
     @Test
@@ -909,7 +912,7 @@
         verifyNoMoreInteractions(mMockWindowManager);
 
         verify(mMockThumbnail)
-                .updateThumbNail(eq(scale), eq(startCenter.x), eq(startCenter.y));
+                .updateThumbnail(eq(scale), eq(startCenter.x), eq(startCenter.y));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
index 60c8148..3baa102 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationThumbnailTest.java
@@ -66,14 +66,14 @@
 
     @Test
     public void updateThumbnailShows() {
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
                 /* centerY= */ 10
         ));
         idle();
 
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2.2f,
                 /* centerX= */ 15,
                 /* centerY= */ 50
@@ -86,7 +86,7 @@
 
     @Test
     public void updateThumbnailLingersThenHidesAfterTimeout() throws InterruptedException {
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
                 /* centerY= */ 10
@@ -103,14 +103,14 @@
 
     @Test
     public void hideThumbnailRemoves() throws InterruptedException {
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
                 /* centerY= */ 10
         ));
         idle();
 
-        runOnMainSync(() -> mMagnificationThumbnail.hideThumbNail());
+        runOnMainSync(() -> mMagnificationThumbnail.hideThumbnail());
         idle();
 
         // Wait for the fade out animation
@@ -122,10 +122,10 @@
 
     @Test
     public void hideShowHideShowHideRemoves() throws InterruptedException {
-        runOnMainSync(() -> mMagnificationThumbnail.hideThumbNail());
+        runOnMainSync(() -> mMagnificationThumbnail.hideThumbnail());
         idle();
 
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
                 /* centerY= */ 10
@@ -135,17 +135,17 @@
         // Wait for the fade in animation
         Thread.sleep(200L);
 
-        runOnMainSync(() -> mMagnificationThumbnail.hideThumbNail());
+        runOnMainSync(() -> mMagnificationThumbnail.hideThumbnail());
         idle();
 
-        runOnMainSync(() -> mMagnificationThumbnail.updateThumbNail(
+        runOnMainSync(() -> mMagnificationThumbnail.updateThumbnail(
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
                 /* centerY= */ 10
         ));
         idle();
 
-        runOnMainSync(() -> mMagnificationThumbnail.hideThumbNail());
+        runOnMainSync(() -> mMagnificationThumbnail.hideThumbnail());
         idle();
 
 
@@ -158,7 +158,7 @@
 
     @Test
     public void hideWithoutShowDoesNothing() throws InterruptedException {
-        runOnMainSync(() -> mMagnificationThumbnail.hideThumbNail());
+        runOnMainSync(() -> mMagnificationThumbnail.hideThumbnail());
         idle();
 
         // Wait for the fade out animation
@@ -172,7 +172,7 @@
 
     @Test
     public void whenHidden_setBoundsDoesNotShow() throws InterruptedException {
-        runOnMainSync(() -> mMagnificationThumbnail.setThumbNailBounds(
+        runOnMainSync(() -> mMagnificationThumbnail.setThumbnailBounds(
                 new Rect(),
                 /* scale=   */ 2f,
                 /* centerX= */ 5,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
index f26c7e6..9d84a07 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java
@@ -135,7 +135,7 @@
     }
 
     @Test
-    public void testUserCanAuthDuringLockoutOfSameSession() {
+    public void testUserLockedDuringLockoutOfSameSession() {
         mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG, 0 /* requestId */);
 
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
@@ -151,9 +151,9 @@
                 0 /* requestId */);
 
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
-                LockoutTracker.LOCKOUT_NONE);
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
-                LockoutTracker.LOCKOUT_NONE);
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
                 LockoutTracker.LOCKOUT_NONE);
     }
@@ -191,9 +191,9 @@
                 0 /* requestId */);
 
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isEqualTo(
-                LockoutTracker.LOCKOUT_NONE);
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_WEAK)).isEqualTo(
-                LockoutTracker.LOCKOUT_NONE);
+                LockoutTracker.LOCKOUT_PERMANENT);
         assertThat(mCoordinator.getLockoutStateFor(PRIMARY_USER, BIOMETRIC_STRONG)).isEqualTo(
                 LockoutTracker.LOCKOUT_NONE);
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java
new file mode 100644
index 0000000..3ed95eb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionServiceTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.datatransfer.contextsync;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.Presubmit;
+import android.telecom.PhoneAccount;
+import android.testing.AndroidTestingRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+public class CallMetadataSyncConnectionServiceTest {
+
+    private CallMetadataSyncConnectionService mSyncConnectionService;
+
+    @Before
+    public void setUp() throws Exception {
+        mSyncConnectionService = new CallMetadataSyncConnectionService() {
+            @Override
+            public String getPackageName() {
+                return "android";
+            }
+        };
+    }
+
+    @Test
+    public void createPhoneAccount_success() {
+        final PhoneAccount phoneAccount = mSyncConnectionService.createPhoneAccount(
+                "com.google.test", "Test App");
+        assertWithMessage("Could not create phone account").that(phoneAccount).isNotNull();
+    }
+
+    @Test
+    public void createPhoneAccount_alreadyExists_doesNotCreateAnother() {
+        final PhoneAccount phoneAccount = mSyncConnectionService.createPhoneAccount(
+                "com.google.test", "Test App");
+        final PhoneAccount phoneAccount2 = mSyncConnectionService.createPhoneAccount(
+                "com.google.test", "Test App #2");
+        assertWithMessage("Could not create phone account").that(phoneAccount).isNotNull();
+        assertWithMessage("Unexpectedly created second phone account").that(phoneAccount2).isNull();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java
new file mode 100644
index 0000000..c5a9af7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncDataTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.datatransfer.contextsync;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.testing.AndroidTestingRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+public class CallMetadataSyncDataTest {
+
+    @Test
+    public void call_writeToParcel_fromParcel_reconstructsSuccessfully() {
+        final CallMetadataSyncData.Call call = new CallMetadataSyncData.Call();
+        final long id = 5;
+        final String callerId = "callerId";
+        final byte[] appIcon = "appIcon".getBytes();
+        final String appName = "appName";
+        final String appIdentifier = "com.google.test";
+        final int status = 1;
+        final int control1 = 2;
+        final int control2 = 3;
+        call.setId(id);
+        call.setCallerId(callerId);
+        call.setAppIcon(appIcon);
+        call.setAppName(appName);
+        call.setAppIdentifier(appIdentifier);
+        call.setStatus(status);
+        call.addControl(control1);
+        call.addControl(control2);
+
+        Parcel parcel = Parcel.obtain();
+        call.writeToParcel(parcel, /* flags= */ 0);
+        parcel.setDataPosition(0);
+        final CallMetadataSyncData.Call reconstructedCall = CallMetadataSyncData.Call.fromParcel(
+                parcel);
+
+        assertThat(reconstructedCall.getId()).isEqualTo(id);
+        assertThat(reconstructedCall.getCallerId()).isEqualTo(callerId);
+        assertThat(reconstructedCall.getAppIcon()).isEqualTo(appIcon);
+        assertThat(reconstructedCall.getAppName()).isEqualTo(appName);
+        assertThat(reconstructedCall.getAppIdentifier()).isEqualTo(appIdentifier);
+        assertThat(reconstructedCall.getStatus()).isEqualTo(status);
+        assertThat(reconstructedCall.getControls()).containsExactly(control1, control2);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index a4a3e36..c8c1d6f 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -1160,6 +1160,7 @@
         final int fd = 1;
         final int keyCode = KeyEvent.KEYCODE_A;
         final int action = VirtualKeyEvent.ACTION_UP;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS,
                 DEVICE_NAME_1, INPUT_DEVICE_ID);
@@ -1167,8 +1168,9 @@
         mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder()
                 .setKeyCode(keyCode)
                 .setAction(action)
+                .setEventTimeNanos(eventTimeNanos)
                 .build());
-        verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action);
+        verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action, eventTimeNanos);
     }
 
     @Test
@@ -1188,14 +1190,17 @@
         final int fd = 1;
         final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
         final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS,
                 DEVICE_NAME_1, INPUT_DEVICE_ID);
         doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
                 .setButtonCode(buttonCode)
-                .setAction(action).build());
-        verify(mNativeWrapperMock).writeButtonEvent(fd, buttonCode, action);
+                .setAction(action)
+                .setEventTimeNanos(eventTimeNanos)
+                .build());
+        verify(mNativeWrapperMock).writeButtonEvent(fd, buttonCode, action, eventTimeNanos);
     }
 
     @Test
@@ -1229,13 +1234,17 @@
         final int fd = 1;
         final float x = -0.2f;
         final float y = 0.7f;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
                 INPUT_DEVICE_ID);
         doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
-                .setRelativeX(x).setRelativeY(y).build());
-        verify(mNativeWrapperMock).writeRelativeEvent(fd, x, y);
+                .setRelativeX(x)
+                .setRelativeY(y)
+                .setEventTimeNanos(eventTimeNanos)
+                .build());
+        verify(mNativeWrapperMock).writeRelativeEvent(fd, x, y, eventTimeNanos);
     }
 
     @Test
@@ -1270,14 +1279,17 @@
         final int fd = 1;
         final float x = 0.5f;
         final float y = 1f;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
                 INPUT_DEVICE_ID);
         doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
         mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
                 .setXAxisMovement(x)
-                .setYAxisMovement(y).build());
-        verify(mNativeWrapperMock).writeScrollEvent(fd, x, y);
+                .setYAxisMovement(y)
+                .setEventTimeNanos(eventTimeNanos)
+                .build());
+        verify(mNativeWrapperMock).writeScrollEvent(fd, x, y, eventTimeNanos);
     }
 
     @Test
@@ -1318,6 +1330,7 @@
         final float x = 100.5f;
         final float y = 200.5f;
         final int action = VirtualTouchEvent.ACTION_UP;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_TOUCHSCREEN, DISPLAY_ID_1, PHYS,
                 DEVICE_NAME_1, INPUT_DEVICE_ID);
@@ -1327,9 +1340,10 @@
                 .setAction(action)
                 .setPointerId(pointerId)
                 .setToolType(toolType)
+                .setEventTimeNanos(eventTimeNanos)
                 .build());
         verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
-                Float.NaN);
+                Float.NaN, eventTimeNanos);
     }
 
     @Test
@@ -1342,6 +1356,7 @@
         final int action = VirtualTouchEvent.ACTION_UP;
         final float pressure = 1.0f;
         final float majorAxisSize = 10.0f;
+        final long eventTimeNanos = 5000L;
         mInputController.addDeviceForTesting(BINDER, fd,
                 InputController.InputDeviceDescriptor.TYPE_TOUCHSCREEN, DISPLAY_ID_1, PHYS,
                 DEVICE_NAME_1, INPUT_DEVICE_ID);
@@ -1353,9 +1368,10 @@
                 .setToolType(toolType)
                 .setPressure(pressure)
                 .setMajorAxisSize(majorAxisSize)
+                .setEventTimeNanos(eventTimeNanos)
                 .build());
         verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, pressure,
-                majorAxisSize);
+                majorAxisSize, eventTimeNanos);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java
index 89ff2c2..5f81869 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessMappingStrategyTest.java
@@ -287,25 +287,37 @@
             adjustedNits50p[i] = DISPLAY_RANGE_NITS[i] * 0.5f;
         }
 
-        // Default is unadjusted
+        // Default
         assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
-                0.0001f /* tolerance */);
+                TOLERANCE);
         assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
-                0.0001f /* tolerance */);
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[0],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
 
-        // When adjustment is turned on, adjustment array is used
+        // Adjustment is turned on
         strategy.recalculateSplines(true, adjustedNits50p);
+        assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
+                TOLERANCE);
         assertEquals(DISPLAY_RANGE_NITS[0] / 2,
-                strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), 0.0001f /* tolerance */);
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
         assertEquals(DISPLAY_RANGE_NITS[1] / 2,
-                strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), 0.0001f /* tolerance */);
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
 
-        // When adjustment is turned off, adjustment array is ignored
+        // Adjustment is turned off
         strategy.recalculateSplines(false, adjustedNits50p);
         assertEquals(DISPLAY_RANGE_NITS[0], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]),
-                0.0001f /* tolerance */);
+                TOLERANCE);
         assertEquals(DISPLAY_RANGE_NITS[1], strategy.convertToNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]),
-                0.0001f /* tolerance */);
+                TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[0],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[0]), TOLERANCE);
+        assertEquals(DISPLAY_RANGE_NITS[1],
+                strategy.convertToAdjustedNits(BACKLIGHT_RANGE_ZERO_TO_ONE[1]), TOLERANCE);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
index cfb432a..d7b12e0 100644
--- a/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/brightness/DisplayBrightnessControllerTest.java
@@ -279,20 +279,27 @@
 
     @Test
     public void testConvertToNits() {
-        float brightness = 0.5f;
-        float nits = 300;
+        final float brightness = 0.5f;
+        final float nits = 300;
+        final float adjustedNits = 200;
 
         // ABC is null
         assertEquals(-1f, mDisplayBrightnessController.convertToNits(brightness),
                 /* delta= */ 0);
+        assertEquals(-1f, mDisplayBrightnessController.convertToAdjustedNits(brightness),
+                /* delta= */ 0);
 
         AutomaticBrightnessController automaticBrightnessController =
                 mock(AutomaticBrightnessController.class);
         when(automaticBrightnessController.convertToNits(brightness)).thenReturn(nits);
+        when(automaticBrightnessController.convertToAdjustedNits(brightness))
+                .thenReturn(adjustedNits);
         mDisplayBrightnessController.setAutomaticBrightnessController(
                 automaticBrightnessController);
 
         assertEquals(nits, mDisplayBrightnessController.convertToNits(brightness), /* delta= */ 0);
+        assertEquals(adjustedNits, mDisplayBrightnessController.convertToAdjustedNits(brightness),
+                /* delta= */ 0);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
index dca67d6..76b6a82 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
@@ -17,6 +17,8 @@
 package com.android.server.power.stats.wakeups;
 
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
 import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_WIFI;
@@ -50,6 +52,8 @@
     private static final String KERNEL_REASON_ALARM_IRQ = "120 test.alarm.device";
     private static final String KERNEL_REASON_WIFI_IRQ = "130 test.wifi.device";
     private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device";
+    private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device";
+    private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device";
     private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
     private static final String KERNEL_REASON_UNKNOWN = "free-form-reason test.alarm.device";
     private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device";
@@ -110,6 +114,83 @@
     }
 
     @Test
+    public void irqAttributionAllCombinations() {
+        final int[] subsystems = new int[] {
+                CPU_WAKEUP_SUBSYSTEM_ALARM,
+                CPU_WAKEUP_SUBSYSTEM_WIFI,
+                CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
+                CPU_WAKEUP_SUBSYSTEM_SENSOR,
+                CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+        };
+
+        final String[] kernelReasons = new String[] {
+                KERNEL_REASON_ALARM_IRQ,
+                KERNEL_REASON_WIFI_IRQ,
+                KERNEL_REASON_SOUND_TRIGGER_IRQ,
+                KERNEL_REASON_SENSOR_IRQ,
+                KERNEL_REASON_CELLULAR_DATA_IRQ,
+        };
+
+        final int[] uids = new int[] {
+                TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5
+        };
+
+        final int[] procStates = new int[] {
+                TEST_PROC_STATE_2,
+                TEST_PROC_STATE_3,
+                TEST_PROC_STATE_4,
+                TEST_PROC_STATE_1,
+                TEST_PROC_STATE_5
+        };
+
+        final int total = subsystems.length;
+        assertThat(kernelReasons.length).isEqualTo(total);
+        assertThat(uids.length).isEqualTo(total);
+        assertThat(procStates.length).isEqualTo(total);
+
+        for (int mask = 1; mask < (1 << total); mask++) {
+            final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3,
+                    mHandler);
+            populateDefaultProcStates(obj);
+
+            final long wakeupTime = mRandom.nextLong(123456);
+
+            String combinedKernelReason = null;
+            for (int i = 0; i < total; i++) {
+                if ((mask & (1 << i)) != 0) {
+                    combinedKernelReason = (combinedKernelReason == null)
+                            ? kernelReasons[i]
+                            : String.join(":", combinedKernelReason, kernelReasons[i]);
+                }
+
+                obj.noteWakingActivity(subsystems[i], wakeupTime + 2, uids[i]);
+            }
+            obj.noteWakeupTimeAndReason(wakeupTime, 1, combinedKernelReason);
+
+            assertThat(obj.mWakeupAttribution.size()).isEqualTo(1);
+
+            final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+            assertThat(attribution.size()).isEqualTo(Integer.bitCount(mask));
+
+            for (int i = 0; i < total; i++) {
+                if ((mask & (1 << i)) == 0) {
+                    assertThat(attribution.contains(subsystems[i])).isFalse();
+                    continue;
+                }
+                assertThat(attribution.contains(subsystems[i])).isTrue();
+                assertThat(attribution.get(subsystems[i]).get(uids[i])).isEqualTo(procStates[i]);
+
+                for (int j = 0; j < total; j++) {
+                    if (i == j) {
+                        continue;
+                    }
+                    assertThat(attribution.get(subsystems[i]).indexOfKey(uids[j])).isLessThan(0);
+                }
+            }
+        }
+    }
+
+    @Test
     public void alarmIrqAttributionSolo() {
         final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
         final long wakeupTime = 12423121;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 8fcbf2f..541739d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -96,6 +96,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 
 public class ManagedServicesTest extends UiServiceTestCase {
 
@@ -1920,6 +1921,18 @@
         assertTrue(service.isBound(cn_disallowed, 0));
     }
 
+    @Test
+    public void isComponentEnabledForCurrentProfiles_isThreadSafe() throws InterruptedException {
+        for (UserInfo userInfo : mUm.getUsers()) {
+            mService.addApprovedList("pkg1/cmp1:pkg2/cmp2:pkg3/cmp3", userInfo.id, true);
+        }
+        testThreadSafety(() -> {
+            mService.rebindServices(false, 0);
+            assertThat(mService.isComponentEnabledForCurrentProfiles(
+                    new ComponentName("pkg1", "cmp1"))).isTrue();
+        }, 20, 30);
+    }
+
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
             ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
             throws RemoteException {
@@ -2298,4 +2311,38 @@
             return false;
         }
     }
+
+    /**
+     * Helper method to test the thread safety of some operations.
+     *
+     * <p>Runs the supplied {@code operationToTest}, {@code nRunsPerThread} times,
+     * concurrently using {@code nThreads} threads, and waits for all of them to finish.
+     */
+    private static void testThreadSafety(Runnable operationToTest, int nThreads,
+            int nRunsPerThread) throws InterruptedException {
+        final CountDownLatch startLatch = new CountDownLatch(1);
+        final CountDownLatch doneLatch = new CountDownLatch(nThreads);
+
+        for (int i = 0; i < nThreads; i++) {
+            Runnable threadRunnable = () -> {
+                try {
+                    startLatch.await();
+                    for (int j = 0; j < nRunsPerThread; j++) {
+                        operationToTest.run();
+                    }
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                } finally {
+                    doneLatch.countDown();
+                }
+            };
+            new Thread(threadRunnable, "Test Thread #" + i).start();
+        }
+
+        // Ready set go
+        startLatch.countDown();
+
+        // Wait for all test threads to be done.
+        doneLatch.await();
+    }
 }
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index f12b53a..fe7cd4a 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -89,6 +89,12 @@
 
         <activity android:name="com.android.server.wm.SurfaceControlViewHostTests$TestActivity" />
 
+        <activity android:name="android.server.wm.scvh.SurfaceSyncGroupActivity"
+            android:screenOrientation="locked"
+            android:turnScreenOn="true"
+            android:theme="@style/WhiteBackgroundTheme"
+            android:exported="true"/>
+
         <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService"
             android:foregroundServiceType="mediaProjection"
             android:enabled="true">
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java
new file mode 100644
index 0000000..9db647a
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.window.SurfaceSyncGroup.TRANSACTION_READY_TIMEOUT;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Instrumentation;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.platform.test.annotations.Presubmit;
+import android.server.wm.scvh.SurfaceSyncGroupActivity;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.cts.surfacevalidator.BitmapPixelChecker;
+import android.window.SurfaceSyncGroup;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+public class SurfaceSyncGroupTests {
+    private static final String TAG = "SurfaceSyncGroupTests";
+
+    @Rule
+    public ActivityTestRule<SurfaceSyncGroupActivity> mActivityRule = new ActivityTestRule<>(
+            SurfaceSyncGroupActivity.class);
+
+    private SurfaceSyncGroupActivity mActivity;
+
+    Instrumentation mInstrumentation;
+
+    private final HandlerThread mHandlerThread = new HandlerThread("applyTransaction");
+    private Handler mHandler;
+
+    @Before
+    public void setup() {
+        mActivity = mActivityRule.getActivity();
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mHandlerThread.start();
+        mHandler = mHandlerThread.getThreadHandler();
+    }
+
+    @Test
+    public void testOverlappingSyncsEnsureOrder_WhenTimeout() throws InterruptedException {
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        params.format = PixelFormat.TRANSLUCENT;
+
+        CountDownLatch secondDrawCompleteLatch = new CountDownLatch(1);
+        CountDownLatch bothSyncGroupsComplete = new CountDownLatch(2);
+        final SurfaceSyncGroup firstSsg = new SurfaceSyncGroup(TAG + "-first");
+        final SurfaceSyncGroup secondSsg = new SurfaceSyncGroup(TAG + "-second");
+        final SurfaceSyncGroup infiniteSsg = new SurfaceSyncGroup(TAG + "-infinite");
+
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.addTransactionCommittedListener(Runnable::run, bothSyncGroupsComplete::countDown);
+        firstSsg.addTransaction(t);
+
+        View backgroundView = mActivity.getBackgroundView();
+        firstSsg.add(backgroundView.getRootSurfaceControl(),
+                () -> mActivity.runOnUiThread(() -> backgroundView.setBackgroundColor(Color.RED)));
+
+        addSecondSyncGroup(secondSsg, secondDrawCompleteLatch, bothSyncGroupsComplete);
+
+        assertTrue("Failed to draw two frames",
+                secondDrawCompleteLatch.await(5, TimeUnit.SECONDS));
+
+        mHandler.postDelayed(() -> {
+            // Don't add a markSyncReady for the first sync group until after it's added to another
+            // SSG to ensure the timeout is longer than the second frame's timeout. The infinite SSG
+            // will never complete to ensure it reaches the timeout, but only after the second SSG
+            // had a chance to reach its timeout.
+            infiniteSsg.add(firstSsg, null /* runnable */);
+            firstSsg.markSyncReady();
+        }, 200);
+
+        assertTrue("Failed to wait for both SurfaceSyncGroups to apply",
+                bothSyncGroupsComplete.await(5, TimeUnit.SECONDS));
+
+        validateScreenshot();
+    }
+
+    @Test
+    public void testOverlappingSyncsEnsureOrder_WhileHoldingTransaction()
+            throws InterruptedException {
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        params.format = PixelFormat.TRANSLUCENT;
+
+        CountDownLatch secondDrawCompleteLatch = new CountDownLatch(1);
+        CountDownLatch bothSyncGroupsComplete = new CountDownLatch(2);
+
+        final SurfaceSyncGroup firstSsg = new SurfaceSyncGroup(TAG + "-first",
+                transaction -> mHandler.postDelayed(() -> {
+                    try {
+                        assertTrue("Failed to draw two frames",
+                                secondDrawCompleteLatch.await(5, TimeUnit.SECONDS));
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException(e);
+                    }
+                    transaction.apply();
+                }, TRANSACTION_READY_TIMEOUT + 200));
+        final SurfaceSyncGroup secondSsg = new SurfaceSyncGroup(TAG + "-second");
+
+        SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+        t.addTransactionCommittedListener(Runnable::run, bothSyncGroupsComplete::countDown);
+        firstSsg.addTransaction(t);
+
+        View backgroundView = mActivity.getBackgroundView();
+        firstSsg.add(backgroundView.getRootSurfaceControl(),
+                () -> mActivity.runOnUiThread(() -> backgroundView.setBackgroundColor(Color.RED)));
+        firstSsg.markSyncReady();
+
+        addSecondSyncGroup(secondSsg, secondDrawCompleteLatch, bothSyncGroupsComplete);
+
+        assertTrue("Failed to wait for both SurfaceSyncGroups to apply",
+                bothSyncGroupsComplete.await(5, TimeUnit.SECONDS));
+
+        validateScreenshot();
+    }
+
+    private void addSecondSyncGroup(SurfaceSyncGroup surfaceSyncGroup,
+            CountDownLatch waitForSecondDraw, CountDownLatch bothSyncGroupsComplete) {
+        View backgroundView = mActivity.getBackgroundView();
+        ViewTreeObserver viewTreeObserver = backgroundView.getViewTreeObserver();
+        viewTreeObserver.registerFrameCommitCallback(() -> mHandler.post(() -> {
+            surfaceSyncGroup.add(backgroundView.getRootSurfaceControl(),
+                    () -> mActivity.runOnUiThread(
+                            () -> backgroundView.setBackgroundColor(Color.BLUE)));
+
+            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+            t.addTransactionCommittedListener(Runnable::run, bothSyncGroupsComplete::countDown);
+            surfaceSyncGroup.addTransaction(t);
+            surfaceSyncGroup.markSyncReady();
+            viewTreeObserver.registerFrameCommitCallback(waitForSecondDraw::countDown);
+        }));
+    }
+
+    private void validateScreenshot() {
+        Bitmap screenshot = mInstrumentation.getUiAutomation().takeScreenshot(
+                mActivity.getWindow());
+        assertNotNull("Failed to generate a screenshot", screenshot);
+        Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false);
+        screenshot.recycle();
+
+        BitmapPixelChecker pixelChecker = new BitmapPixelChecker(Color.BLUE);
+        int halfWidth = swBitmap.getWidth() / 2;
+        int halfHeight = swBitmap.getHeight() / 2;
+        // We don't need to check all the pixels since we only care that at least some of them are
+        // blue. If the buffers were submitted out of order, all the pixels will be red.
+        Rect bounds = new Rect(halfWidth, halfHeight, halfWidth + 10, halfHeight + 10);
+        int numMatchingPixels = pixelChecker.getNumMatchingPixels(swBitmap, bounds);
+        assertEquals("Expected 100 received " + numMatchingPixels + " matching pixels", 100,
+                numMatchingPixels);
+
+        swBitmap.recycle();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 43b429c..653b52b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -109,14 +109,19 @@
     final SurfaceControl.Transaction mMockT = mock(SurfaceControl.Transaction.class);
     private BLASTSyncEngine mSyncEngine;
 
+    private Transition createTestTransition(int transitType, TransitionController controller) {
+        return new Transition(transitType, 0 /* flags */, controller, controller.mSyncEngine);
+    }
+
     private Transition createTestTransition(int transitType) {
         final TransitionController controller = new TestTransitionController(
                 mock(ActivityTaskManagerService.class));
 
         mSyncEngine = createTestBLASTSyncEngine();
-        final Transition t = new Transition(transitType, 0 /* flags */, controller, mSyncEngine);
-        t.startCollecting(0 /* timeoutMs */);
-        return t;
+        controller.setSyncEngine(mSyncEngine);
+        final Transition out = createTestTransition(transitType, controller);
+        out.startCollecting(0 /* timeoutMs */);
+        return out;
     }
 
     @Test
@@ -367,7 +372,6 @@
             final ActivityRecord act = createActivityRecord(tasks[i]);
             // alternate so that the transition doesn't get promoted to the display area
             act.setVisibleRequested((i % 2) == 0); // starts invisible
-            act.visibleIgnoringKeyguard = (i % 2) == 0;
             if (i == showWallpaperTask) {
                 doReturn(true).when(act).showWallpaper();
             }
@@ -754,10 +758,8 @@
 
         final ActivityRecord closing = createActivityRecord(oldTask);
         closing.setOccludesParent(true);
-        closing.visibleIgnoringKeyguard = true;
         final ActivityRecord opening = createActivityRecord(newTask);
         opening.setOccludesParent(true);
-        opening.visibleIgnoringKeyguard = true;
         // Start states.
         changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
         changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, false /* exChg */));
@@ -795,10 +797,8 @@
 
         final ActivityRecord closing = createActivityRecord(oldTask);
         closing.setOccludesParent(true);
-        closing.visibleIgnoringKeyguard = true;
         final ActivityRecord opening = createActivityRecord(newTask);
         opening.setOccludesParent(false);
-        opening.visibleIgnoringKeyguard = true;
         // Start states.
         changes.put(newTask, new Transition.ChangeInfo(newTask, false /* vis */, true /* exChg */));
         changes.put(oldTask, new Transition.ChangeInfo(oldTask, true /* vis */, false /* exChg */));
@@ -837,10 +837,8 @@
 
         final ActivityRecord closing = closingTaskFragment.getTopMostActivity();
         closing.setOccludesParent(true);
-        closing.visibleIgnoringKeyguard = true;
         final ActivityRecord opening = openingTaskFragment.getTopMostActivity();
         opening.setOccludesParent(true);
-        opening.visibleIgnoringKeyguard = true;
         // Start states.
         changes.put(openingTaskFragment, new Transition.ChangeInfo(openingTaskFragment,
                 false /* vis */, true /* exChg */));
@@ -881,10 +879,8 @@
 
         final ActivityRecord closing = closingTaskFragment.getTopMostActivity();
         closing.setOccludesParent(true);
-        closing.visibleIgnoringKeyguard = true;
         final ActivityRecord opening = openingTaskFragment.getTopMostActivity();
         opening.setOccludesParent(false);
-        opening.visibleIgnoringKeyguard = true;
         // Start states.
         changes.put(openingTaskFragment, new Transition.ChangeInfo(openingTaskFragment,
                 false /* vis */, true /* exChg */));
@@ -925,10 +921,8 @@
 
         final ActivityRecord opening = openingTaskFragment.getTopMostActivity();
         opening.setOccludesParent(true);
-        opening.visibleIgnoringKeyguard = true;
         final ActivityRecord closing = closingTaskFragment.getTopMostActivity();
         closing.setOccludesParent(true);
-        closing.visibleIgnoringKeyguard = true;
         closing.finishing = true;
         // Start states.
         changes.put(openingTaskFragment, new Transition.ChangeInfo(openingTaskFragment,
@@ -970,10 +964,8 @@
 
         final ActivityRecord opening = openingTaskFragment.getTopMostActivity();
         opening.setOccludesParent(true);
-        opening.visibleIgnoringKeyguard = true;
         final ActivityRecord closing = closingTaskFragment.getTopMostActivity();
         closing.setOccludesParent(false);
-        closing.visibleIgnoringKeyguard = true;
         closing.finishing = true;
         // Start states.
         changes.put(openingTaskFragment, new Transition.ChangeInfo(openingTaskFragment,
@@ -1282,6 +1274,7 @@
     @Test
     public void testIntermediateVisibility() {
         final TransitionController controller = new TestTransitionController(mAtm);
+        controller.setSyncEngine(mWm.mSyncEngine);
         final ITransitionPlayer player = new ITransitionPlayer.Default();
         controller.registerTransitionPlayer(player, null /* playerProc */);
         final Transition openTransition = controller.createTransition(TRANSIT_OPEN);
@@ -1365,6 +1358,7 @@
                 super.dispatchLegacyAppTransitionFinished(ar);
             }
         };
+        controller.setSyncEngine(mWm.mSyncEngine);
         controller.mSnapshotController = mWm.mSnapshotController;
         final TaskSnapshotController taskSnapshotController = controller.mSnapshotController
                 .mTaskSnapshotController;
@@ -1462,6 +1456,7 @@
     @Test
     public void testNotReadyPushPop() {
         final TransitionController controller = new TestTransitionController(mAtm);
+        controller.setSyncEngine(mWm.mSyncEngine);
         final ITransitionPlayer player = new ITransitionPlayer.Default();
         controller.registerTransitionPlayer(player, null /* playerProc */);
         final Transition openTransition = controller.createTransition(TRANSIT_OPEN);
@@ -1918,6 +1913,110 @@
         assertEquals(TRANSIT_CHANGE, info.getChanges().get(0).getMode());
     }
 
+    @Test
+    public void testQueueStartCollect() {
+        final TransitionController controller = mAtm.getTransitionController();
+        final TestTransitionPlayer player = registerTestTransitionPlayer();
+
+        mSyncEngine = createTestBLASTSyncEngine();
+        controller.setSyncEngine(mSyncEngine);
+
+        final Transition transitA = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitB = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitC = createTestTransition(TRANSIT_OPEN, controller);
+
+        final boolean[] onStartA = new boolean[]{false, false};
+        final boolean[] onStartB = new boolean[]{false, false};
+        controller.startCollectOrQueue(transitA, (deferred) -> {
+            onStartA[0] = true;
+            onStartA[1] = deferred;
+        });
+        controller.startCollectOrQueue(transitB, (deferred) -> {
+            onStartB[0] = true;
+            onStartB[1] = deferred;
+        });
+        waitUntilHandlersIdle();
+
+        assertTrue(onStartA[0]);
+        assertFalse(onStartA[1]);
+        assertTrue(transitA.isCollecting());
+
+        // B should be queued, so no calls yet
+        assertFalse(onStartB[0]);
+        assertTrue(transitB.isPending());
+
+        // finish collecting A
+        transitA.start();
+        transitA.setAllReady();
+        mSyncEngine.tryFinishForTest(transitA.getSyncId());
+        waitUntilHandlersIdle();
+
+        assertTrue(transitA.isPlaying());
+        assertTrue(transitB.isCollecting());
+        assertTrue(onStartB[0]);
+        // Should receive deferred = true
+        assertTrue(onStartB[1]);
+
+        // finish collecting B
+        transitB.start();
+        transitB.setAllReady();
+        mSyncEngine.tryFinishForTest(transitB.getSyncId());
+        assertTrue(transitB.isPlaying());
+
+        // Now we should be able to start collecting directly a new transition
+        final boolean[] onStartC = new boolean[]{false, false};
+        controller.startCollectOrQueue(transitC, (deferred) -> {
+            onStartC[0] = true;
+            onStartC[1] = deferred;
+        });
+        waitUntilHandlersIdle();
+        assertTrue(onStartC[0]);
+        assertFalse(onStartC[1]);
+        assertTrue(transitC.isCollecting());
+    }
+
+    @Test
+    public void testQueueWithLegacy() {
+        final TransitionController controller = mAtm.getTransitionController();
+        final TestTransitionPlayer player = registerTestTransitionPlayer();
+
+        mSyncEngine = createTestBLASTSyncEngine();
+        controller.setSyncEngine(mSyncEngine);
+
+        final Transition transitA = createTestTransition(TRANSIT_OPEN, controller);
+        final Transition transitB = createTestTransition(TRANSIT_OPEN, controller);
+
+        controller.startCollectOrQueue(transitA, (deferred) -> {});
+
+        BLASTSyncEngine.SyncGroup legacySync = mSyncEngine.prepareSyncSet(
+                mock(BLASTSyncEngine.TransactionReadyListener.class), "test");
+        final boolean[] applyLegacy = new boolean[]{false};
+        controller.startLegacySyncOrQueue(legacySync, () -> applyLegacy[0] = true);
+        assertFalse(applyLegacy[0]);
+        waitUntilHandlersIdle();
+
+        controller.startCollectOrQueue(transitB, (deferred) -> {});
+        assertTrue(transitA.isCollecting());
+
+        // finish collecting A
+        transitA.start();
+        transitA.setAllReady();
+        mSyncEngine.tryFinishForTest(transitA.getSyncId());
+        waitUntilHandlersIdle();
+
+        assertTrue(transitA.isPlaying());
+        // legacy sync should start now
+        assertTrue(applyLegacy[0]);
+        // transitB must wait
+        assertTrue(transitB.isPending());
+
+        // finish legacy sync
+        mSyncEngine.setReady(legacySync.mSyncId);
+        mSyncEngine.tryFinishForTest(legacySync.mSyncId);
+        // transitioncontroller should be notified so it can start collecting B
+        assertTrue(transitB.isCollecting());
+    }
+
     private static void makeTaskOrganized(Task... tasks) {
         final ITaskOrganizer organizer = mock(ITaskOrganizer.class);
         for (Task t : tasks) {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
index aadb38d..2e05e20 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java
@@ -81,7 +81,12 @@
     void informRestartProcessLocked() {
         Slog.v(TAG, "informRestartProcessLocked");
         mUpdateStateAfterStartFinished.set(false);
-        //TODO(b/261783819): Starts detection in VisualQueryDetectionService.
+        try {
+            mCallback.onProcessRestarted();
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to communicate #onProcessRestarted", e);
+            notifyOnDetectorRemoteException();
+        }
     }
 
     void setVisualQueryDetectionAttentionListenerLocked(
diff --git a/telecomm/java/android/telecom/CallStreamingService.java b/telecomm/java/android/telecom/CallStreamingService.java
index 3f538a7..df48cd6 100644
--- a/telecomm/java/android/telecom/CallStreamingService.java
+++ b/telecomm/java/android/telecom/CallStreamingService.java
@@ -52,6 +52,7 @@
  * </service>
  * }
  * </pre>
+ *
  * @hide
  */
 @SystemApi
@@ -65,7 +66,7 @@
     private static final int MSG_SET_STREAMING_CALL_ADAPTER = 1;
     private static final int MSG_CALL_STREAMING_STARTED = 2;
     private static final int MSG_CALL_STREAMING_STOPPED = 3;
-    private static final int MSG_CALL_STREAMING_CHANGED_CHANGED = 4;
+    private static final int MSG_CALL_STREAMING_STATE_CHANGED = 4;
 
     /** Default Handler used to consolidate binder method calls onto a single thread. */
     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -77,8 +78,10 @@
 
             switch (msg.what) {
                 case MSG_SET_STREAMING_CALL_ADAPTER:
-                    mStreamingCallAdapter = new StreamingCallAdapter(
-                            (IStreamingCallAdapter) msg.obj);
+                    if (msg.obj != null) {
+                        mStreamingCallAdapter = new StreamingCallAdapter(
+                                (IStreamingCallAdapter) msg.obj);
+                    }
                     break;
                 case MSG_CALL_STREAMING_STARTED:
                     mCall = (StreamingCall) msg.obj;
@@ -90,10 +93,12 @@
                     mStreamingCallAdapter = null;
                     CallStreamingService.this.onCallStreamingStopped();
                     break;
-                case MSG_CALL_STREAMING_CHANGED_CHANGED:
+                case MSG_CALL_STREAMING_STATE_CHANGED:
                     int state = (int) msg.obj;
-                    mCall.requestStreamingState(state);
-                    CallStreamingService.this.onCallStreamingStateChanged(state);
+                    if (mStreamingCallAdapter != null) {
+                        mCall.requestStreamingState(state);
+                        CallStreamingService.this.onCallStreamingStateChanged(state);
+                    }
                     break;
                 default:
                     break;
@@ -128,7 +133,7 @@
 
         @Override
         public void onCallStreamingStateChanged(int state) throws RemoteException {
-            mHandler.obtainMessage(MSG_CALL_STREAMING_CHANGED_CHANGED, state).sendToTarget();
+            mHandler.obtainMessage(MSG_CALL_STREAMING_STATE_CHANGED, state).sendToTarget();
         }
     }
 
@@ -173,9 +178,12 @@
                     STREAMING_FAILED_ALREADY_STREAMING,
                     STREAMING_FAILED_NO_SENDER,
                     STREAMING_FAILED_SENDER_BINDING_ERROR
-    })
+            })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface StreamingFailedReason {};
+    public @interface StreamingFailedReason {
+    }
+
+    ;
 
     /**
      * Called when a {@code StreamingCall} has been added to this call streaming session. The call
diff --git a/telephony/java/android/telephony/AnomalyReporter.java b/telephony/java/android/telephony/AnomalyReporter.java
index d676eee..db38f88 100644
--- a/telephony/java/android/telephony/AnomalyReporter.java
+++ b/telephony/java/android/telephony/AnomalyReporter.java
@@ -105,6 +105,8 @@
      * @param carrierId the carrier of the id associated with this event.
      */
     public static void reportAnomaly(@NonNull UUID eventId, String description, int carrierId) {
+        Rlog.i(TAG, "reportAnomaly: Received anomaly event report with eventId= " + eventId
+                + " and description= " + description);
         if (sContext == null) {
             Rlog.w(TAG, "AnomalyReporter not yet initialized, dropping event=" + eventId);
             return;
diff --git a/telephony/java/android/telephony/TelephonyScanManager.java b/telephony/java/android/telephony/TelephonyScanManager.java
index 19f2a9b..b761709 100644
--- a/telephony/java/android/telephony/TelephonyScanManager.java
+++ b/telephony/java/android/telephony/TelephonyScanManager.java
@@ -153,8 +153,9 @@
                     nsi = mScanInfo.get(message.arg2);
                 }
                 if (nsi == null) {
-                    throw new RuntimeException(
-                        "Failed to find NetworkScanInfo with id " + message.arg2);
+                    Rlog.e(TAG, "Unexpceted message " + message.what
+                            + " as there is no NetworkScanInfo with id " + message.arg2);
+                    return;
                 }
 
                 final NetworkScanCallback callback = nsi.mCallback;