Merge "fwb: Introduce constant for assist hw button." into udc-dev
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index 051dde0..b732da2 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -127,6 +127,7 @@
     private BroadcastWaiter mBroadcastWaiter;
     private UserSwitchWaiter mUserSwitchWaiter;
     private String mUserSwitchTimeoutMs;
+    private String mDisableUserSwitchingDialogAnimations;
 
     private final BenchmarkRunner mRunner = new BenchmarkRunner();
     @Rule
@@ -153,16 +154,17 @@
             Log.w(TAG, "WARNING: Tests are being run from user " + mAm.getCurrentUser()
                     + " rather than the system user");
         }
-        mUserSwitchTimeoutMs = setSystemProperty("debug.usercontroller.user_switch_timeout_ms",
-                "100000");
-        if (TextUtils.isEmpty(mUserSwitchTimeoutMs)) {
-            mUserSwitchTimeoutMs = "invalid";
-        }
+        mUserSwitchTimeoutMs = setSystemProperty(
+                "debug.usercontroller.user_switch_timeout_ms", "100000");
+        mDisableUserSwitchingDialogAnimations = setSystemProperty(
+                "debug.usercontroller.disable_user_switching_dialog_animations", "true");
     }
 
     @After
     public void tearDown() throws Exception {
         setSystemProperty("debug.usercontroller.user_switch_timeout_ms", mUserSwitchTimeoutMs);
+        setSystemProperty("debug.usercontroller.disable_user_switching_dialog_animations",
+                mDisableUserSwitchingDialogAnimations);
         mBroadcastWaiter.close();
         mUserSwitchWaiter.close();
         for (int userId : mUsersToRemove) {
@@ -1538,7 +1540,7 @@
     private String setSystemProperty(String name, String value) throws Exception {
         final String oldValue = ShellHelper.runShellCommand("getprop " + name);
         assertEquals("", ShellHelper.runShellCommand("setprop " + name + " " + value));
-        return oldValue;
+        return TextUtils.firstNotEmpty(oldValue, "invalid");
     }
 
     private void waitForBroadcastIdle() {
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/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl
index d62e15a..3249b41 100644
--- a/core/java/android/app/IActivityTaskManager.aidl
+++ b/core/java/android/app/IActivityTaskManager.aidl
@@ -174,6 +174,9 @@
     ActivityTaskManager.RootTaskInfo getFocusedRootTaskInfo();
     Rect getTaskBounds(int taskId);
 
+    /** Focuses the top task on a display if it isn't already focused. Used for Recents. */
+    void focusTopTask(int displayId);
+
     void cancelRecentsAnimation(boolean restoreHomeRootTaskPosition);
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.UPDATE_LOCK_TASK_PACKAGES)")
     void updateLockTaskPackages(int userId, in String[] packages);
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/camera2/impl/CameraExtensionJpegProcessor.java b/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java
index 7701125..875550a 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionJpegProcessor.java
@@ -46,6 +46,7 @@
 public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl {
     public final static String TAG = "CameraExtensionJpeg";
     private final static int JPEG_QUEUE_SIZE = 1;
+    private final static int JPEG_APP_SEGMENT_SIZE = 64 * 1024;
 
     private final Handler mHandler;
     private final HandlerThread mHandlerThread;
@@ -243,9 +244,10 @@
     private void initializePipeline() throws RemoteException {
         if ((mFormat != -1) && (mOutputSurface != null) && (mResolution != null) &&
                 (mYuvReader == null)) {
-            // Jpeg/blobs are expected to be configured with (w*h)x1
+            // Jpeg/blobs are expected to be configured with (w*h)x1.5 + 64k Jpeg APP1 segment
             mOutputWriter = ImageWriter.newInstance(mOutputSurface, 1 /*maxImages*/,
-                    ImageFormat.JPEG, mResolution.width * mResolution.height, 1);
+                    ImageFormat.JPEG,
+                    (mResolution.width * mResolution.height * 3)/2 + JPEG_APP_SEGMENT_SIZE, 1);
             mYuvReader = ImageReader.newInstance(mResolution.width, mResolution.height, mFormat,
                     JPEG_QUEUE_SIZE);
             mYuvReader.setOnImageAvailableListener(
diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java
index 3bc240c..0221296 100644
--- a/core/java/android/hardware/face/FaceManager.java
+++ b/core/java/android/hardware/face/FaceManager.java
@@ -348,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 84de1a6..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;
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/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/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 77f3b1d..dd4f964 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -305,6 +306,9 @@
         mImm.startStylusHandwriting(view);
         mState.mHasInitiatedHandwriting = true;
         mState.mShouldInitHandwriting = false;
+        if (view instanceof TextView) {
+            ((TextView) view).hideHint();
+        }
     }
 
     /**
@@ -323,6 +327,9 @@
                 mState.mHasInitiatedHandwriting = true;
                 mState.mShouldInitHandwriting = false;
             }
+            if (view instanceof TextView) {
+                ((TextView) view).hideHint();
+            }
             return true;
         }
         return false;
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/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
index 13ac329..fa0052c 100644
--- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
@@ -840,6 +840,7 @@
         mAnchorId = AccessibilityNodeInfo.UNDEFINED_NODE_ID;
         mTitle = null;
         mTransitionTime = 0;
+        mLocales = LocaleList.getEmptyLocaleList();
     }
 
     /**
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 3fbb505..34fe935 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -806,6 +806,7 @@
     private CharSequence mHint;
     @UnsupportedAppUsage
     private Layout mHintLayout;
+    private boolean mHideHint;
 
     private MovementMethod mMovement;
 
@@ -7180,6 +7181,8 @@
         sendOnTextChanged(text, 0, oldlen, textLength);
         onTextChanged(text, 0, oldlen, textLength);
 
+        mHideHint = false;
+
         if (a11yTextChangeType == AccessibilityUtils.TEXT) {
             notifyViewAccessibilityStateChangedIfNeeded(
                     AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
@@ -7338,6 +7341,7 @@
     }
 
     private void setHintInternal(CharSequence hint) {
+        mHideHint = false;
         mHint = TextUtils.stringOrSpannedString(hint);
 
         if (mLayout != null) {
@@ -7379,6 +7383,19 @@
     }
 
     /**
+     * Temporarily hides the hint text until the text is modified, or the hint text is modified, or
+     * the view gains or loses focus.
+     *
+     * @hide
+     */
+    public void hideHint() {
+        if (isShowingHint()) {
+            mHideHint = true;
+            invalidate();
+        }
+    }
+
+    /**
      * Returns if the text is constrained to a single horizontally scrolling line ignoring new
      * line characters instead of letting it wrap onto multiple lines.
      *
@@ -8974,7 +8991,7 @@
 
         Layout layout = mLayout;
 
-        if (mHint != null && mText.length() == 0) {
+        if (mHint != null && !mHideHint && mText.length() == 0) {
             if (mHintTextColor != null) {
                 color = mCurHintTextColor;
             }
@@ -11293,7 +11310,7 @@
     }
 
     private boolean isShowingHint() {
-        return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint);
+        return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint) && !mHideHint;
     }
 
     /**
@@ -12437,6 +12454,7 @@
         sendOnTextChanged(buffer, start, before, after);
         onTextChanged(buffer, start, before, after);
 
+        mHideHint = false;
         clearGesturePreviewHighlight();
     }
 
@@ -12577,6 +12595,8 @@
             return;
         }
 
+        mHideHint = false;
+
         if (mEditor != null) mEditor.onFocusChanged(focused, direction);
 
         if (focused) {
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/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java
index 0a0785e..80f540c 100644
--- a/core/java/com/android/internal/jank/FrameTracker.java
+++ b/core/java/com/android/internal/jank/FrameTracker.java
@@ -36,6 +36,7 @@
 import android.os.Handler;
 import android.os.Trace;
 import android.text.TextUtils;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Choreographer;
@@ -43,7 +44,9 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.JankData.JankType;
 import android.view.ThreadedRenderer;
+import android.view.View;
 import android.view.ViewRootImpl;
+import android.view.WindowCallbacks;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
@@ -686,6 +689,14 @@
         }
     }
 
+    ThreadedRendererWrapper getThreadedRenderer() {
+        return mRendererWrapper;
+    }
+
+    ViewRootWrapper getViewRoot() {
+        return mViewRoot;
+    }
+
     private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
         boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
                 && missedFramesCount >= mTraceThresholdMissedFrames;
@@ -798,6 +809,28 @@
         public SurfaceControl getSurfaceControl() {
             return mViewRoot.getSurfaceControl();
         }
+
+        void requestInvalidateRootRenderNode() {
+            mViewRoot.requestInvalidateRootRenderNode();
+        }
+
+        void addWindowCallbacks(WindowCallbacks windowCallbacks) {
+            mViewRoot.addWindowCallbacks(windowCallbacks);
+        }
+
+        void removeWindowCallbacks(WindowCallbacks windowCallbacks) {
+            mViewRoot.removeWindowCallbacks(windowCallbacks);
+        }
+
+        View getView() {
+            return mViewRoot.getView();
+        }
+
+        int dipToPx(int dip) {
+            final DisplayMetrics displayMetrics =
+                    mViewRoot.mContext.getResources().getDisplayMetrics();
+            return (int) (displayMetrics.density * dip + 0.5f);
+        }
     }
 
     public static class SurfaceControlWrapper {
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 4b9e77e..c769fb9 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -97,6 +97,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION;
 
 import android.Manifest;
+import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
@@ -104,6 +105,7 @@
 import android.annotation.WorkerThread;
 import android.app.ActivityThread;
 import android.content.Context;
+import android.graphics.Color;
 import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerExecutor;
@@ -143,6 +145,14 @@
  * adb shell device_config put interaction_jank_monitor enabled true
  * adb shell device_config put interaction_jank_monitor sampling_interval 1
  *
+ * On debuggable builds, an overlay can be used to display the name of the
+ * currently running cuj using:
+ *
+ * adb shell device_config put interaction_jank_monitor debug_overlay_enabled true
+ *
+ * NOTE: The overlay will interfere with metrics, so it should only be used
+ * for understanding which UI events correspeond to which CUJs.
+ *
  * @hide
  */
 public class InteractionJankMonitor {
@@ -159,6 +169,7 @@
             "trace_threshold_missed_frames";
     private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY =
             "trace_threshold_frame_time_millis";
+    private static final String SETTINGS_DEBUG_OVERLAY_ENABLED_KEY = "debug_overlay_enabled";
     /** Default to being enabled on debug builds. */
     private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
     /** Default to collecting data for all CUJs. */
@@ -166,6 +177,7 @@
     /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */
     private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3;
     private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64;
+    private static final boolean DEFAULT_DEBUG_OVERLAY_ENABLED = false;
 
     @VisibleForTesting
     public static final int MAX_LENGTH_OF_CUJ_NAME = 80;
@@ -343,6 +355,9 @@
     private final HandlerThread mWorker;
     private final DisplayResolutionTracker mDisplayResolutionTracker;
     private final Object mLock = new Object();
+    private @ColorInt int mDebugBgColor = Color.CYAN;
+    private double mDebugYOffset = 0.1;
+    private InteractionMonitorDebugOverlay mDebugOverlay;
 
     private volatile boolean mEnabled = DEFAULT_ENABLED;
     private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -533,7 +548,7 @@
         if (needRemoveTasks(action, session)) {
             getTracker(session.getCuj()).getHandler().runWithScissors(() -> {
                 removeTimeout(session.getCuj());
-                removeTracker(session.getCuj());
+                removeTracker(session.getCuj(), session.getReason());
             }, EXECUTOR_TASK_TIMEOUT);
         }
     }
@@ -699,7 +714,7 @@
         if (tracker == null) return false;
         // if the end call doesn't return true, another thread is handling end of the cuj.
         if (tracker.end(REASON_END_NORMAL)) {
-            removeTracker(cujType);
+            removeTracker(cujType, REASON_END_NORMAL);
         }
         return true;
     }
@@ -750,7 +765,7 @@
         if (tracker == null) return false;
         // if the cancel call doesn't return true, another thread is handling cancel of the cuj.
         if (tracker.cancel(reason)) {
-            removeTracker(cujType);
+            removeTracker(cujType, reason);
         }
         return true;
     }
@@ -758,6 +773,13 @@
     private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) {
         synchronized (mLock) {
             mRunningTrackers.put(cuj, tracker);
+            if (mDebugOverlay != null) {
+                mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot());
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj)
+                        + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers));
+            }
         }
     }
 
@@ -767,9 +789,16 @@
         }
     }
 
-    private void removeTracker(@CujType int cuj) {
+    private void removeTracker(@CujType int cuj, int reason) {
         synchronized (mLock) {
             mRunningTrackers.remove(cuj);
+            if (mDebugOverlay != null) {
+                mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers);
+            }
+            if (DEBUG) {
+                Log.d(TAG, "Removed tracker for " + getNameOfCuj(cuj)
+                        + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers));
+            }
         }
     }
 
@@ -782,6 +811,16 @@
         mTraceThresholdFrameTimeMillis = properties.getInt(
                 SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY,
                 DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS);
+        // Never allow the debug overlay to be used on user builds
+        boolean debugOverlayEnabled = Build.IS_DEBUGGABLE && properties.getBoolean(
+                SETTINGS_DEBUG_OVERLAY_ENABLED_KEY,
+                DEFAULT_DEBUG_OVERLAY_ENABLED);
+        if (debugOverlayEnabled && mDebugOverlay == null) {
+            mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset);
+        } else if (!debugOverlayEnabled && mDebugOverlay != null) {
+            mDebugOverlay.dispose();
+            mDebugOverlay = null;
+        }
         // The memory visibility is powered by the volatile field, mEnabled.
         mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
     }
@@ -822,6 +861,39 @@
     }
 
     /**
+     * Configures the debug overlay used for displaying interaction names on the screen while they
+     * occur.
+     *
+     * @param bgColor the background color of the box used to display the CUJ names
+     * @param yOffset number between 0 and 1 to indicate where the top of the box should be relative
+     *                to the height of the screen
+     */
+    public void configDebugOverlay(@ColorInt int bgColor, double yOffset) {
+        mDebugBgColor = bgColor;
+        mDebugYOffset = yOffset;
+    }
+
+    /**
+     * A helper method for getting a string representation of all running CUJs. For example,
+     * "(LOCKSCREEN_TRANSITION_FROM_AOD, IME_INSETS_ANIMATION)"
+     */
+    private static String listNamesOfCujs(SparseArray<FrameTracker> trackers) {
+        if (!DEBUG) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        sb.append('(');
+        for (int i = 0; i < trackers.size(); i++) {
+            sb.append(getNameOfCuj(trackers.keyAt(i)));
+            if (i < trackers.size() - 1) {
+                sb.append(", ");
+            }
+        }
+        sb.append(')');
+        return sb.toString();
+    }
+
+    /**
      * A helper method to translate CUJ type to CUJ name.
      *
      * @param cujType the cuj type defined in this file
diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
new file mode 100644
index 0000000..99b9f2f
--- /dev/null
+++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
@@ -0,0 +1,240 @@
+/*
+ * 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.internal.jank;
+
+import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
+
+import android.annotation.ColorInt;
+import android.app.ActivityThread;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.os.Trace;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.WindowCallbacks;
+
+import com.android.internal.jank.FrameTracker.Reasons;
+import com.android.internal.jank.InteractionJankMonitor.CujType;
+
+/**
+ * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window
+ * associated with one of the CUJs being tracked. There's no guarantee which window it will
+ * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they
+ * are actually running.
+ * <p>
+ * CUJ names will be drawn as follows:
+ * <ul>
+ * <li> Normal text indicates the CUJ is currently running
+ * <li> Grey text indicates the CUJ ended normally and is no longer running
+ * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally
+ * </ul>
+ */
+class InteractionMonitorDebugOverlay implements WindowCallbacks {
+    private static final int REASON_STILL_RUNNING = -1000;
+    // Sparse array where the key in the CUJ and the value is the session status, or null if
+    // it's currently running
+    private final SparseIntArray mRunningCujs = new SparseIntArray();
+    private FrameTracker.ViewRootWrapper mViewRoot = null;
+    private final Paint mDebugPaint;
+    private final Paint.FontMetrics mDebugFontMetrics;
+    // Used to display the overlay in a different color and position for different processes.
+    // Otherwise, two overlays will overlap and be difficult to read.
+    private final int mBgColor;
+    private final double mYOffset;
+    private final String mPackageName;
+
+    InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) {
+        mBgColor = bgColor;
+        mYOffset = yOffset;
+        mDebugPaint = new Paint();
+        mDebugPaint.setAntiAlias(false);
+        mDebugFontMetrics = new Paint.FontMetrics();
+        final Context context = ActivityThread.currentApplication();
+        mPackageName = context.getPackageName();
+    }
+
+    void dispose() {
+        if (mViewRoot != null) {
+            mViewRoot.removeWindowCallbacks(this);
+            forceRedraw();
+        }
+        mViewRoot = null;
+    }
+
+    private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) {
+        if (mViewRoot == null && viewRoot != null) {
+            mViewRoot = viewRoot;
+            viewRoot.addWindowCallbacks(this);
+            forceRedraw();
+            return true;
+        }
+        return false;
+    }
+
+    private float getWidthOfLongestCujName(int cujFontSize) {
+        mDebugPaint.setTextSize(cujFontSize);
+        float maxLength = 0;
+        for (int i = 0; i < mRunningCujs.size(); i++) {
+            String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i));
+            float textLength = mDebugPaint.measureText(cujName);
+            if (textLength > maxLength) {
+                maxLength = textLength;
+            }
+        }
+        return maxLength;
+    }
+
+    private float getTextHeight(int textSize) {
+        mDebugPaint.setTextSize(textSize);
+        mDebugPaint.getFontMetrics(mDebugFontMetrics);
+        return mDebugFontMetrics.descent - mDebugFontMetrics.ascent;
+    }
+
+    private int dipToPx(int dip) {
+        if (mViewRoot != null) {
+            return mViewRoot.dipToPx(dip);
+        } else {
+            return dip;
+        }
+    }
+
+    private void forceRedraw() {
+        if (mViewRoot != null) {
+            mViewRoot.requestInvalidateRootRenderNode();
+            mViewRoot.getView().invalidate();
+        }
+    }
+
+    void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason,
+                          SparseArray<FrameTracker> runningTrackers) {
+        mRunningCujs.put(removedCuj, reason);
+        // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
+        if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
+            mRunningCujs.clear();
+            dispose();
+        } else {
+            boolean needsNewViewRoot = true;
+            if (mViewRoot != null) {
+                // Check to see if this viewroot is still associated with one of the running
+                // trackers
+                for (int i = 0; i < runningTrackers.size(); i++) {
+                    if (mViewRoot.equals(
+                            runningTrackers.valueAt(i).getViewRoot())) {
+                        needsNewViewRoot = false;
+                        break;
+                    }
+                }
+            }
+            if (needsNewViewRoot) {
+                dispose();
+                for (int i = 0; i < runningTrackers.size(); i++) {
+                    if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) {
+                        break;
+                    }
+                }
+            } else {
+                forceRedraw();
+            }
+        }
+    }
+
+    void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) {
+        // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
+        // is still running
+        mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
+        attachViewRootIfNeeded(viewRoot);
+        forceRedraw();
+    }
+
+    @Override
+    public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen,
+                                       Rect systemInsets, Rect stableInsets) {
+    }
+
+    @Override
+    public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen,
+                                        Rect systemInsets, Rect stableInsets) {
+    }
+
+    @Override
+    public void onWindowDragResizeEnd() {
+    }
+
+    @Override
+    public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
+        return false;
+    }
+
+    @Override
+    public void onRequestDraw(boolean reportNextDraw) {
+    }
+
+    @Override
+    public void onPostDraw(RecordingCanvas canvas) {
+        Trace.beginSection("InteractionJankMonitor#drawDebug");
+        final int padding = dipToPx(5);
+        final int h = canvas.getHeight();
+        final int w = canvas.getWidth();
+        // Draw sysui CUjs near the bottom of the screen so they don't overlap with the shade,
+        // and draw launcher CUJs near the top of the screen so they don't overlap with gestures
+        final int dy = (int) (h * mYOffset);
+        int packageNameFontSize = dipToPx(12);
+        int cujFontSize = dipToPx(18);
+        final float cujNameTextHeight = getTextHeight(cujFontSize);
+        final float packageNameTextHeight = getTextHeight(packageNameFontSize);
+        float maxLength = getWidthOfLongestCujName(cujFontSize);
+
+        final int dx = (int) ((w - maxLength) / 2f);
+        canvas.translate(dx, dy);
+        // Draw background rectangle for displaying the text showing the CUJ name
+        mDebugPaint.setColor(mBgColor);
+        canvas.drawRect(
+                -padding * 2, // more padding on top so we can draw the package name
+                -padding,
+                padding * 2 + maxLength,
+                padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(),
+                mDebugPaint);
+        mDebugPaint.setTextSize(packageNameFontSize);
+        mDebugPaint.setColor(Color.BLACK);
+        mDebugPaint.setStrikeThruText(false);
+        canvas.translate(0, packageNameTextHeight);
+        canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint);
+        mDebugPaint.setTextSize(cujFontSize);
+        // Draw text for CUJ names
+        for (int i = 0; i < mRunningCujs.size(); i++) {
+            int status = mRunningCujs.valueAt(i);
+            if (status == REASON_STILL_RUNNING) {
+                mDebugPaint.setColor(Color.BLACK);
+                mDebugPaint.setStrikeThruText(false);
+            } else if (status == REASON_END_NORMAL) {
+                mDebugPaint.setColor(Color.GRAY);
+                mDebugPaint.setStrikeThruText(false);
+            } else {
+                // Cancelled, or otherwise ended for a bad reason
+                mDebugPaint.setColor(Color.RED);
+                mDebugPaint.setStrikeThruText(true);
+            }
+            String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i));
+            canvas.translate(0, cujNameTextHeight);
+            canvas.drawText(cujName, 0, 0, mDebugPaint);
+        }
+        Trace.endSection();
+    }
+}
diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp
index 55aa711..4474d4ca 100644
--- a/core/jni/android_graphics_BLASTBufferQueue.cpp
+++ b/core/jni/android_graphics_BLASTBufferQueue.cpp
@@ -52,7 +52,7 @@
     return env;
 }
 
-  struct {
+struct {
     jmethodID onTransactionHang;
 } gTransactionHangCallback;
 
@@ -72,12 +72,14 @@
     }
 
     void onTransactionHang(const std::string& reason) {
-        if (mTransactionHangObject) {
-            JNIEnv* env = getenv(mVm);
-            ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str()));
-            getenv(mVm)->CallVoidMethod(mTransactionHangObject,
-                                        gTransactionHangCallback.onTransactionHang, jReason.get());
+        if (!mTransactionHangObject) {
+            return;
         }
+        JNIEnv* env = getenv(mVm);
+        ScopedLocalRef<jstring> jReason(env, env->NewStringUTF(reason.c_str()));
+        getenv(mVm)->CallVoidMethod(mTransactionHangObject,
+                                    gTransactionHangCallback.onTransactionHang, jReason.get());
+        DieIfException(env, "Uncaught exception in TransactionHangCallback.");
     }
 
 private:
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/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index e42c6f1..8e96ac1 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -285,6 +285,7 @@
         JNIEnv* env = getenv();
         env->CallVoidMethod(mTransactionCommittedListenerObject,
                             gTransactionCommittedListenerClassInfo.onTransactionCommitted);
+        DieIfException(env, "Uncaught exception in TransactionCommittedListener.");
     }
 
     static void transactionCallbackThunk(void* context, nsecs_t /*latchTime*/,
@@ -325,6 +326,7 @@
     binder::Status onWindowInfosReported() override {
         JNIEnv* env = getenv();
         env->CallVoidMethod(mListener, gRunnableClassInfo.run);
+        DieIfException(env, "Uncaught exception in WindowInfosReportedListener.");
         return binder::Status::ok();
     }
 
@@ -356,6 +358,7 @@
         env->CallVoidMethod(mTrustedPresentationCallback,
                             gTrustedPresentationCallbackClassInfo.onTrustedPresentationChanged,
                             inTrustedPresentationState);
+        DieIfException(env, "Uncaught exception in TrustedPresentationCallback.");
     }
 
     void addCallbackRef(const sp<SurfaceComposerClient::PresentationCallbackRAII>& callbackRef) {
diff --git a/core/jni/core_jni_helpers.h b/core/jni/core_jni_helpers.h
index b85a425..210dc89 100644
--- a/core/jni/core_jni_helpers.h
+++ b/core/jni/core_jni_helpers.h
@@ -134,6 +134,15 @@
     return env;
 }
 
+static inline void DieIfException(JNIEnv* env, const char* message) {
+    if (env->ExceptionCheck()) {
+        jnihelp::ExpandableString summary;
+        jnihelp::ExpandableStringInitialize(&summary);
+        jnihelp::GetStackTraceOrSummary(env, nullptr, &summary);
+        LOG_ALWAYS_FATAL("%s\n%s", message, summary.data);
+    }
+}
+
 }  // namespace android
 
 #endif  // CORE_JNI_HELPERS
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 e216f88..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
diff --git a/core/res/res/drawable/loading_spinner.xml b/core/res/res/drawable/loading_spinner.xml
new file mode 100644
index 0000000..49603d8
--- /dev/null
+++ b/core/res/res/drawable/loading_spinner.xml
@@ -0,0 +1,55 @@
+<!-- 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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+                 xmlns:aapt="http://schemas.android.com/aapt">
+    <aapt:attr name="android:drawable">
+        <vector android:height="230dp" android:width="230dp" android:viewportHeight="230"
+                android:viewportWidth="230">
+            <group android:name="_R_G">
+                <group android:name="_R_G_L_0_G" android:translateX="100.621"
+                       android:translateY="102.621">
+                    <path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#ffffff"
+                          android:strokeLineCap="round" android:strokeLineJoin="round"
+                          android:strokeWidth="8" android:strokeAlpha="1" android:trimPathStart="0"
+                          android:trimPathEnd="0" android:trimPathOffset="0"
+                          android:pathData=" M14.38 -93.62 C72.88,-93.62 120.38,-46.12 120.38,12.38 C120.38,70.88 72.88,118.38 14.38,118.38 C-44.12,118.38 -91.62,70.88 -91.62,12.38 C-91.62,-46.12 -44.12,-93.62 14.38,-93.62c "/>
+                </group>
+            </group>
+            <group android:name="time_group"/>
+        </vector>
+    </aapt:attr>
+    <target android:name="_R_G_L_0_G_D_0_P_0">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="trimPathEnd" android:duration="350"
+                                android:startOffset="0" android:valueFrom="0" android:valueTo="1"
+                                android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.4,1 1.0,1.0"/>
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="time_group">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:propertyName="translateX" android:duration="517"
+                                android:startOffset="0" android:valueFrom="0" android:valueTo="1"
+                                android:valueType="floatType"/>
+            </set>
+        </aapt:attr>
+    </target>
+</animated-vector>
\ No newline at end of file
diff --git a/core/res/res/layout/user_switching_dialog.xml b/core/res/res/layout/user_switching_dialog.xml
index 2e041f5..496179a 100644
--- a/core/res/res/layout/user_switching_dialog.xml
+++ b/core/res/res/layout/user_switching_dialog.xml
@@ -14,17 +14,48 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:id="@+id/content"
+             android:background="?attr/colorBackground"
+             android:layout_width="match_parent"
+             android:layout_height="match_parent">
 
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/message"
-        style="?attr/textAppearanceListItem"
-        android:background="?attr/colorSurface"
+    <LinearLayout
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
         android:gravity="center"
-        android:drawablePadding="12dp"
-        android:drawableTint="?attr/textColorPrimary"
-        android:paddingStart="?attr/dialogPreferredPadding"
-        android:paddingEnd="?attr/dialogPreferredPadding"
-        android:paddingTop="24dp"
-        android:paddingBottom="24dp" />
+        android:orientation="vertical"
+        android:paddingBottom="77dp">
+
+        <RelativeLayout
+            android:layout_width="242dp"
+            android:layout_height="242dp"
+            android:layout_gravity="center">
+
+            <ImageView
+                android:id="@+id/icon"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_margin="26dp" />
+
+            <ImageView
+                android:id="@+id/progress_circular"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_margin="6dp"
+                android:src="@drawable/loading_spinner" />
+
+        </RelativeLayout>
+
+        <TextView xmlns:android="http://schemas.android.com/apk/res/android"
+                  android:id="@+id/message"
+                  style="?attr/textAppearanceListItem"
+                  android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:textSize="20sp"
+                  android:textAlignment="center"
+                  android:drawableTint="?attr/textColorPrimary" />
+
+    </LinearLayout>
+</FrameLayout>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5544701..c5f7ea6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1917,7 +1917,8 @@
     <string name="config_defaultNetworkRecommendationProviderPackage" translatable="false"></string>
 
     <!-- The package name of the default search selector app. Must be granted the POST_NOTIFICATIONS
-         permission.
+         permission, and notifications from this app must be given the notification flag
+         FLAG_NO_DISMISS if the notification requests FLAG_ONGOING_EVENT.
     -->
     <string name="config_defaultSearchSelectorPackageName" translatable="false"></string>
 
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 3ea1592..34b4c51 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -91,8 +91,8 @@
         ":BstatsTestApp",
         ":BinderDeathRecipientHelperApp1",
         ":BinderDeathRecipientHelperApp2",
+        ":com.android.cts.helpers.aosp",
     ],
-    required: ["com.android.cts.helpers.aosp"],
 }
 
 // Rules to copy all the test apks to the intermediate raw resource directory
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/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index 8ae6381..c0125af 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -23,14 +23,20 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.Instrumentation;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
 import android.view.HandwritingInitiator;
@@ -38,7 +44,9 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
@@ -47,6 +55,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 
 /**
  * Tests for {@link HandwritingInitiator}
@@ -543,6 +552,111 @@
         assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView1);
     }
 
+    @Test
+    public void startHandwriting_hidesHint() {
+        EditText editText =
+                new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
+        editText.setHint("hint");
+        editText.setLayoutParams(new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+        verifyEditTextDrawsText(editText, "hint");
+
+        mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
+        mHandwritingInitiator.startHandwriting(editText);
+
+        verifyEditTextDrawsText(editText, null);
+    }
+
+    @Test
+    public void startHandwriting_clearFocus_restoresHint() {
+        EditText editText =
+                new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
+        editText.setHint("hint");
+        editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
+        editText.requestFocus();
+
+        verifyEditTextDrawsText(editText, "hint");
+
+        mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
+        mHandwritingInitiator.startHandwriting(editText);
+
+        verifyEditTextDrawsText(editText, null);
+
+        editText.clearFocus();
+
+        verifyEditTextDrawsText(editText, "hint");
+    }
+
+    @Test
+    public void startHandwriting_setHint_restoresHint() {
+        EditText editText =
+                new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
+        editText.setHint("hint");
+        editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
+
+        verifyEditTextDrawsText(editText, "hint");
+
+        mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
+        mHandwritingInitiator.startHandwriting(editText);
+
+        verifyEditTextDrawsText(editText, null);
+
+        editText.setHint("new hint");
+
+        verifyEditTextDrawsText(editText, "new hint");
+    }
+
+    @Test
+    public void startHandwriting_setText_restoresHint() {
+        EditText editText =
+                new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
+        editText.setHint("hint");
+        editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
+
+        verifyEditTextDrawsText(editText, "hint");
+
+        mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
+        mHandwritingInitiator.startHandwriting(editText);
+
+        verifyEditTextDrawsText(editText, null);
+
+        editText.setText("a");
+        editText.setText("");
+
+        verifyEditTextDrawsText(editText, "hint");
+    }
+
+    private void verifyEditTextDrawsText(EditText editText, String text) {
+        editText.measure(
+                View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST),
+                View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST));
+        Canvas canvas = prepareMockCanvas(editText);
+        editText.draw(canvas);
+        if (text != null) {
+            ArgumentCaptor<CharSequence> textCaptor = ArgumentCaptor.forClass(CharSequence.class);
+            verify(canvas).drawText(
+                    textCaptor.capture(), anyInt(), anyInt(), anyFloat(), anyFloat(), any());
+            assertThat(textCaptor.getValue().toString()).isEqualTo(text);
+        } else {
+            verify(canvas, never()).drawText(
+                    any(CharSequence.class), anyInt(), anyInt(), anyFloat(), anyFloat(), any());
+        }
+    }
+
+    private Canvas prepareMockCanvas(View view) {
+        Canvas canvas = mock(Canvas.class);
+        when(canvas.getClipBounds(any())).thenAnswer(invocation -> {
+            Rect outRect = invocation.getArgument(0);
+            outRect.top = 0;
+            outRect.left = 0;
+            outRect.right = view.getMeasuredWidth();
+            outRect.bottom = view.getMeasuredHeight();
+            return true;
+        });
+        return canvas;
+    }
+
     private MotionEvent createStylusEvent(int action, int x, int y, long eventTime) {
         MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1);
         properties[0].toolType = MotionEvent.TOOL_TYPE_STYLUS;
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
index 03d366e6..57a1376 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -46,6 +46,10 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.os.Handler;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IAccessibilityManager;
 
@@ -57,6 +61,7 @@
 import com.android.internal.R;
 import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -73,8 +78,14 @@
 @RunWith(AndroidJUnit4.class)
 public class AccessibilityShortcutChooserActivityTest {
     private static final String ONE_HANDED_MODE = "One-Handed mode";
+    private static final String DENY_LABEL = "Deny";
+    private static final String EDIT_LABEL = "Edit shortcuts";
+    private static final String LIST_TITLE_LABEL = "Choose features to use";
     private static final String TEST_LABEL = "TEST_LABEL";
     private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class");
+    private static final long UI_TIMEOUT_MS = 1000;
+    private UiDevice mDevice;
+    private ActivityScenario<TestAccessibilityShortcutChooserActivity> mScenario;
     private TestAccessibilityShortcutChooserActivity mActivity;
 
     @Rule
@@ -92,6 +103,8 @@
 
     @Before
     public void setUp() throws Exception {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.wakeUp();
         when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo);
         mResolveInfo.serviceInfo = mServiceInfo;
         mServiceInfo.applicationInfo = mApplicationInfo;
@@ -102,27 +115,36 @@
         when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
                 anyString(), anyInt(), anyInt())).thenReturn(true);
         TestAccessibilityShortcutChooserActivity.setupForTesting(mAccessibilityManagerService);
+        mScenario = ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
+        mScenario.onActivity(activity -> mActivity = activity);
+        mScenario.moveToState(Lifecycle.State.CREATED);
+        mScenario.moveToState(Lifecycle.State.STARTED);
+        mScenario.moveToState(Lifecycle.State.RESUMED);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    @After
+    public void cleanUp() {
+        mScenario.moveToState(Lifecycle.State.DESTROYED);
     }
 
     @Test
     public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() {
-        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
-                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
-        scenario.moveToState(Lifecycle.State.CREATED);
-        scenario.moveToState(Lifecycle.State.STARTED);
-        scenario.moveToState(Lifecycle.State.RESUMED);
+        openShortcutsList();
 
-        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
-                isDialog()).check(matches(isDisplayed()));
-        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
-        onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
-        onView(withId(R.id.accessibility_permission_enable_deny_button)).perform(scrollTo(),
-                click());
+        // Performing the double-click is flaky so retry if needed.
+        for (int attempt = 1; attempt <= 2; attempt++) {
+            onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
+            if (mDevice.wait(Until.hasObject(By.text(DENY_LABEL)), UI_TIMEOUT_MS)) {
+                break;
+            }
+        }
+
+        onView(withText(DENY_LABEL)).perform(scrollTo(), click());
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check(
                 doesNotExist());
-        scenario.moveToState(Lifecycle.State.DESTROYED);
     }
 
     @Test
@@ -130,60 +152,42 @@
             throws Exception {
         when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
                 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt())).thenReturn(false);
-        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
-                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
-        scenario.onActivity(activity -> mActivity = activity);
-        scenario.moveToState(Lifecycle.State.CREATED);
-        scenario.moveToState(Lifecycle.State.STARTED);
-        scenario.moveToState(Lifecycle.State.RESUMED);
-
-        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
-                isDialog()).check(matches(isDisplayed()));
-        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        openShortcutsList();
 
         onView(withText(TEST_LABEL)).perform(scrollTo(), click());
+
         verify(mAccessibilityManagerService).sendRestrictedDialogIntent(
                 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt());
-        scenario.moveToState(Lifecycle.State.DESTROYED);
     }
 
     @Test
     public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() {
         TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true);
-        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
-                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
-        scenario.moveToState(Lifecycle.State.CREATED);
-        scenario.moveToState(Lifecycle.State.STARTED);
-        scenario.moveToState(Lifecycle.State.RESUMED);
+        openShortcutsList();
 
-        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
-                isDialog()).check(matches(isDisplayed()));
-        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
         onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp());
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        mDevice.wait(Until.hasObject(By.text(ONE_HANDED_MODE)), UI_TIMEOUT_MS);
 
         onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(matches(isDisplayed()));
-        scenario.moveToState(Lifecycle.State.DESTROYED);
     }
 
     @Test
     public void popEditShortcutMenuList_oneHandedModeDisabled_shouldNotBeInListView() {
         TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false);
-        final ActivityScenario<TestAccessibilityShortcutChooserActivity> scenario =
-                ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class);
-        scenario.moveToState(Lifecycle.State.CREATED);
-        scenario.moveToState(Lifecycle.State.STARTED);
-        scenario.moveToState(Lifecycle.State.RESUMED);
+        openShortcutsList();
 
-        onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot(
-                isDialog()).check(matches(isDisplayed()));
-        onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click());
         onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp());
         InstrumentationRegistry.getInstrumentation().waitForIdleSync();
 
         onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(doesNotExist());
-        scenario.moveToState(Lifecycle.State.DESTROYED);
+    }
+
+    private void openShortcutsList() {
+        UiObject2 editButton = mDevice.findObject(By.text(EDIT_LABEL));
+        if (editButton != null) {
+            editButton.click();
+        }
+        mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), UI_TIMEOUT_MS);
     }
 
     /**
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/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/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 1b20f67..f8f8897 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
@@ -1068,6 +1068,7 @@
                     // We need to be Z ordered on top in order for alpha animations to work.
                     mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
                     mExpandedBubble.getExpandedView().setAnimating(true);
+                    mExpandedViewContainer.setVisibility(VISIBLE);
                 }
             }
 
@@ -2922,14 +2923,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 +2957,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) {
@@ -3115,7 +3117,7 @@
                     mAnimatingOutBubbleBuffer.getColorSpace());
 
             mAnimatingOutSurfaceView.setAlpha(1f);
-            mExpandedViewContainer.setVisibility(View.GONE);
+            mExpandedViewContainer.setVisibility(View.INVISIBLE);
 
             mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
                 post(() -> {
@@ -3145,9 +3147,6 @@
         int[] paddings = mPositioner.getExpandedViewContainerPadding(
                 mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
         mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
-        if (mIsExpansionAnimating) {
-            mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
-        }
         if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
             PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
                     getState());
@@ -3272,6 +3271,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/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index cbd544c..e732a03 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -16,9 +16,12 @@
 
 package com.android.wm.shell.desktopmode;
 
+import android.graphics.Region;
+
 import com.android.wm.shell.common.annotations.ExternalThread;
 
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Interface to interact with desktop mode feature in shell.
@@ -32,7 +35,16 @@
      * @param listener the listener to add.
      * @param callbackExecutor the executor to call the listener on.
      */
-    void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+    void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
             Executor callbackExecutor);
 
+    /**
+     * Adds a consumer to listen for Desktop task corner changes. This is used for gesture
+     * exclusion. The SparseArray contains a list of four corner resize handles mapped to each
+     * desktop task's taskId. The resize handle Rects are stored in the following order:
+     * left-top, left-bottom, right-top, right-bottom.
+     */
+    default void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,
+            Executor callbackExecutor) { }
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 2bdbde1..86ea725 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -34,6 +34,7 @@
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.database.ContentObserver;
+import android.graphics.Region;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
@@ -69,6 +70,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Handles windowing changes when desktop mode system setting changes
@@ -149,11 +151,21 @@
      * @param listener the listener to add.
      * @param callbackExecutor the executor to call the listener on.
      */
-    public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+    public void addVisibleTasksListener(DesktopModeTaskRepository.VisibleTasksListener listener,
             Executor callbackExecutor) {
         mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
     }
 
+    /**
+     * Adds a listener to track changes to corners of desktop mode tasks.
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    public void addTaskCornerListener(Consumer<Region> listener,
+            Executor callbackExecutor) {
+        mDesktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor);
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -312,6 +324,23 @@
     }
 
     /**
+     * Update corner rects stored for a specific task
+     * @param taskId task to update
+     * @param taskCorners task's new corner handles
+     */
+    public void onTaskCornersChanged(int taskId, Region taskCorners) {
+        mDesktopModeTaskRepository.updateTaskCorners(taskId, taskCorners);
+    }
+
+    /**
+     * Remove corners saved for a task. Likely used due to task closure.
+     * @param taskId task to remove
+     */
+    public void removeCornersForTask(int taskId) {
+        mDesktopModeTaskRepository.removeTaskCorners(taskId);
+    }
+
+    /**
      * Moves a specifc task to the front.
      * @param taskInfo the task to show in front.
      */
@@ -426,10 +455,19 @@
     private final class DesktopModeImpl implements DesktopMode {
 
         @Override
-        public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+        public void addVisibleTasksListener(
+                DesktopModeTaskRepository.VisibleTasksListener listener,
                 Executor callbackExecutor) {
             mMainExecutor.execute(() -> {
-                DesktopModeController.this.addListener(listener, callbackExecutor);
+                DesktopModeController.this.addVisibleTasksListener(listener, callbackExecutor);
+            });
+        }
+
+        @Override
+        public void addDesktopGestureExclusionRegionListener(Consumer<Region> listener,
+                Executor callbackExecutor) {
+            mMainExecutor.execute(() -> {
+                DesktopModeController.this.addTaskCornerListener(listener, callbackExecutor);
             });
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 47342c9..12f8ea2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,9 +16,13 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.graphics.Region
 import android.util.ArrayMap
 import android.util.ArraySet
+import android.util.SparseArray
+import androidx.core.util.valueIterator
 import java.util.concurrent.Executor
+import java.util.function.Consumer
 
 /**
  * Keeps track of task data related to desktop mode.
@@ -38,6 +42,10 @@
     private val activeTasksListeners = ArraySet<ActiveTasksListener>()
     // Track visible tasks separately because a task may be part of the desktop but not visible.
     private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
+    // Track corners of desktop tasks, used to determine gesture exclusion
+    private val desktopCorners = SparseArray<Region>()
+    private var desktopGestureExclusionListener: Consumer<Region>? = null
+    private var desktopGestureExclusionExecutor: Executor? = null
 
     /**
      * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
@@ -56,6 +64,28 @@
     }
 
     /**
+     * Add a Consumer which will inform other classes of changes to corners for all Desktop tasks.
+     */
+    fun setTaskCornerListener(cornersListener: Consumer<Region>, executor: Executor) {
+        desktopGestureExclusionListener = cornersListener
+        desktopGestureExclusionExecutor = executor
+        executor.execute {
+            desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+        }
+    }
+
+    /**
+     * Create a new merged region representative of all corners in all desktop tasks.
+     */
+    private fun calculateDesktopExclusionRegion(): Region {
+        val desktopCornersRegion = Region()
+        desktopCorners.valueIterator().forEach { taskCorners ->
+            desktopCornersRegion.op(taskCorners, Region.Op.UNION)
+        }
+        return desktopCornersRegion
+    }
+
+    /**
      * Remove a previously registered [ActiveTasksListener]
      */
     fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
@@ -167,6 +197,28 @@
     }
 
     /**
+     * Updates the active desktop corners; if desktopCorners has been accepted by
+     * desktopCornersListener, it will be updated in the appropriate classes.
+     */
+    fun updateTaskCorners(taskId: Int, taskCorners: Region) {
+        desktopCorners.put(taskId, taskCorners)
+        desktopGestureExclusionExecutor?.execute {
+            desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+        }
+    }
+
+    /**
+     * Removes the active desktop corners for the specified task; if desktopCorners has been
+     * accepted by desktopCornersListener, it will be updated in the appropriate classes.
+     */
+    fun removeTaskCorners(taskId: Int) {
+        desktopCorners.delete(taskId)
+        desktopGestureExclusionExecutor?.execute {
+            desktopGestureExclusionListener?.accept(calculateDesktopExclusionRegion())
+        }
+    }
+
+    /**
      * Defines interface for classes that can listen to changes for active tasks in desktop mode.
      */
     interface ActiveTasksListener {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 0400963..0d56023 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -27,6 +27,7 @@
 import android.content.Context
 import android.graphics.Point
 import android.graphics.Rect
+import android.graphics.Region
 import android.os.IBinder
 import android.os.SystemProperties
 import android.view.SurfaceControl
@@ -487,6 +488,20 @@
         return 2 * getStatusBarHeight(taskInfo)
     }
 
+    /**
+     * Update the corner region for a specified task
+     */
+    fun onTaskCornersChanged(taskId: Int, corner: Region) {
+        desktopModeTaskRepository.updateTaskCorners(taskId, corner)
+    }
+
+    /**
+     * Remove a previously tracked corner region for a specified task.
+     */
+    fun removeCornersForTask(taskId: Int) {
+        desktopModeTaskRepository.removeTaskCorners(taskId)
+    }
+
 
     /**
      * Adds a listener to find out about changes in the visibility of freeform tasks.
@@ -494,20 +509,47 @@
      * @param listener the listener to add.
      * @param callbackExecutor the executor to call the listener on.
      */
-    fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
+    fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
         desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor)
     }
 
+    /**
+     * Adds a listener to track changes to desktop task corners
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    fun setTaskCornerListener(
+            listener: Consumer<Region>,
+            callbackExecutor: Executor
+    ) {
+        desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor)
+    }
+
     /** The interface for calls from outside the shell, within the host process. */
     @ExternalThread
     private inner class DesktopModeImpl : DesktopMode {
-        override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
+        override fun addVisibleTasksListener(
+                listener: VisibleTasksListener,
+                callbackExecutor: Executor
+        ) {
             mainExecutor.execute {
-                this@DesktopTasksController.addListener(listener, callbackExecutor)
+                this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
+            }
+        }
+
+        override fun addDesktopGestureExclusionRegionListener(
+                listener: Consumer<Region>,
+                callbackExecutor: Executor
+        ) {
+            mainExecutor.execute {
+                this@DesktopTasksController.setTaskCornerListener(listener, callbackExecutor)
             }
         }
     }
 
+
+
     /** The interface for calls from outside the host process. */
     @BinderThread
     private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
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..eb4d2a1 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
@@ -36,6 +36,7 @@
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.Slog;
+import android.view.Display;
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationTarget;
@@ -100,6 +101,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();
@@ -131,7 +133,7 @@
     public WindowContainerTransaction handleRequest(IBinder transition,
             TransitionRequestInfo request) {
         // do not directly handle requests. Only entry point should be via startRecentsTransition
-        Slog.e(TAG, "RecentsTransitionHandler.handleRequest: Unexpected transition request");
+        // TODO: Only log an error if the transition is a recents transition
         return null;
     }
 
@@ -169,7 +171,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 +183,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;
@@ -207,6 +220,7 @@
         private int mRecentsTaskId = -1;
         private TransitionInfo mInfo = null;
         private boolean mOpeningSeparateHome = false;
+        private boolean mPausingSeparateHome = false;
         private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null;
         private PictureInPictureSurfaceTransaction mPipTransaction = null;
         private IBinder mTransition = null;
@@ -223,10 +237,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 +254,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 +266,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 +307,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 +318,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 +343,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;
@@ -389,6 +409,10 @@
                         // raise closing (pausing) task to "above" layer so it isn't covered
                         t.setLayer(target.leash, info.getChanges().size() * 3 - i);
                         mPausingTasks.add(new TaskState(change, target.leash));
+                        if (taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+                            // This can only happen if we have a separate recents/home (3p launcher)
+                            mPausingSeparateHome = true;
+                        }
                         if (taskInfo.pictureInPictureParams != null
                                 && taskInfo.pictureInPictureParams.isAutoEnterEnabled()) {
                             mPipTask = taskInfo.token;
@@ -409,7 +433,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 +450,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;
@@ -565,6 +591,8 @@
                         final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo);
                         t.reparent(appearedTargets[i].leash, mInfo.getRoot(rootIdx).getLeash());
                         t.setLayer(appearedTargets[i].leash, layer);
+                        // Hide the animation leash, let listener show it.
+                        t.hide(appearedTargets[i].leash);
                         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
                                 "  opening new taskId=%d", appearedTargets[i].taskId);
                         mOpeningTasks.add(new TaskState(change, appearedTargets[i].leash));
@@ -592,13 +620,14 @@
             t.apply();
             // not using the incoming anim-only surfaces
             info.releaseAnimSurfaces();
-            if (appearedTargets == null) return;
-            try {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                        "RecentsController.merge: calling onTasksAppeared");
-                mListener.onTasksAppeared(appearedTargets);
-            } catch (RemoteException e) {
-                Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
+            if (appearedTargets != null) {
+                try {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId);
+                    mListener.onTasksAppeared(appearedTargets);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
+                }
             }
             finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
         }
@@ -620,7 +649,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);
@@ -637,14 +666,17 @@
                             mFinishCB != null, enabled);
                     return;
                 }
+                final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId()
+                        : Display.DEFAULT_DISPLAY;
                 // transient launches don't receive focus automatically. Since we are taking over
                 // the gesture now, take focus explicitly.
                 // This also moves recents back to top if the user gestured before a switch
                 // animation finished.
                 try {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
-                            "RecentsController.setInputConsumerEnabled: set focus to recents");
-                    ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId);
+                            "[%d] RecentsController.setInputConsumerEnabled: set focus to recents",
+                            mInstanceId);
+                    ActivityTaskManager.getService().focusTopTask(displayId);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Failed to set focused task", e);
                 }
@@ -659,7 +691,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 +711,9 @@
                 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 state=%d",
+                    mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState);
             final Transitions.TransitionFinishCallback finishCB = mFinishCB;
             mFinishCB = null;
 
@@ -690,8 +724,19 @@
                 if (toHome) wct.reorder(mRecentsTask, true /* toTop */);
                 else wct.restoreTransientOrder(mRecentsTask);
             }
-            if (!toHome && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL) {
-                ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "  returning to app");
+            if (!toHome
+                    // If a recents gesture starts on the 3p launcher, then the 3p launcher is the
+                    // live tile (pausing app). If the gesture is "cancelled" we need to return to
+                    // 3p launcher instead of "task-switching" away from it.
+                    && (!mWillFinishToHome || mPausingSeparateHome)
+                    && mPausingTasks != null && mState == STATE_NORMAL) {
+                if (mPausingSeparateHome) {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "  returning to 3p home");
+                } else {
+                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+                            "  returning to app");
+                }
                 // The gesture is returning to the pausing-task(s) rather than continuing with
                 // recents, so end the transition by moving the app back to the top (and also
                 // re-showing it's task).
@@ -781,7 +826,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/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index e4f2724..ceef373 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -89,6 +89,9 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
 import android.util.Log;
 import android.util.Slog;
 import android.view.Choreographer;
@@ -2390,6 +2393,7 @@
             if (!mMainStage.isActive()) return false;
 
             mSplitLayout.setFreezeDividerWindow(false);
+            final StageChangeRecord record = new StageChangeRecord();
             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
                 final TransitionInfo.Change change = info.getChanges().get(iC);
                 if (change.getMode() == TRANSIT_CHANGE
@@ -2405,20 +2409,28 @@
                     if (!stage.containsTask(taskInfo.taskId)) {
                         Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
                                 + " with " + taskInfo.taskId + " before startAnimation().");
+                        record.addRecord(stage, true, taskInfo.taskId);
                     }
                 } else if (isClosingType(change.getMode())) {
                     if (stage.containsTask(taskInfo.taskId)) {
+                        record.addRecord(stage, false, taskInfo.taskId);
                         Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
                                 + " with " + taskInfo.taskId + " before startAnimation().");
                     }
                 }
             }
-            if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
+            // If the size of dismissStages > 0, the task is closed without prepare pending
+            // transition, which could happen if all activities were finished after finish top
+            // activity in a task, so the trigger task is null when handleRequest.
+            final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
+            if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0
+                    || dismissStages.size() > 0) {
                 Log.e(TAG, "Somehow removed the last task in a stage outside of a proper "
                         + "transition.");
                 final WindowContainerTransaction wct = new WindowContainerTransaction();
-                final int dismissTop = mMainStage.getChildCount() == 0
-                        ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
+                final int dismissTop = (dismissStages.size() == 1
+                        && getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN)
+                        || mMainStage.getChildCount() == 0 ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
                 prepareExitSplitScreen(dismissTop, wct);
                 mSplitTransitions.startDismissTransition(wct, this, dismissTop,
                         EXIT_REASON_UNKNOWN);
@@ -2445,6 +2457,57 @@
                 finishCallback);
     }
 
+    static class StageChangeRecord {
+        static class StageChange {
+            final StageTaskListener mStageTaskListener;
+            final IntArray mAddedTaskId = new IntArray();
+            final IntArray mRemovedTaskId = new IntArray();
+            StageChange(StageTaskListener stage) {
+                mStageTaskListener = stage;
+            }
+
+            boolean shouldDismissStage() {
+                if (mAddedTaskId.size() > 0 || mRemovedTaskId.size() == 0) {
+                    return false;
+                }
+                int removeChildTaskCount = 0;
+                for (int i = mRemovedTaskId.size() - 1; i >= 0; --i) {
+                    if (mStageTaskListener.containsTask(mRemovedTaskId.get(i))) {
+                        ++removeChildTaskCount;
+                    }
+                }
+                return removeChildTaskCount == mStageTaskListener.getChildCount();
+            }
+        }
+        private final ArrayMap<StageTaskListener, StageChange> mChanges = new ArrayMap<>();
+
+        void addRecord(StageTaskListener stage, boolean open, int taskId) {
+            final StageChange next;
+            if (!mChanges.containsKey(stage)) {
+                next = new StageChange(stage);
+                mChanges.put(stage, next);
+            } else {
+                next = mChanges.get(stage);
+            }
+            if (open) {
+                next.mAddedTaskId.add(taskId);
+            } else {
+                next.mRemovedTaskId.add(taskId);
+            }
+        }
+
+        ArraySet<StageTaskListener> getShouldDismissedStage() {
+            final ArraySet<StageTaskListener> dismissTarget = new ArraySet<>();
+            for (int i = mChanges.size() - 1; i >= 0; --i) {
+                final StageChange change = mChanges.valueAt(i);
+                if (change.shouldDismissStage()) {
+                    dismissTarget.add(change.mStageTaskListener);
+                }
+            }
+            return dismissTarget;
+        }
+    }
+
     /** Starts the pending transition animation. */
     public boolean startPendingAnimation(@NonNull IBinder transition,
             @NonNull TransitionInfo info,
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/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 49a5eac..8fb56fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -36,6 +36,7 @@
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.Region;
 import android.hardware.input.InputManager;
 import android.os.Handler;
 import android.os.IBinder;
@@ -69,6 +70,7 @@
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.TaskCornersListener;
 
 import java.util.Optional;
 import java.util.function.Supplier;
@@ -95,9 +97,11 @@
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
 
+    private final TaskCornersListener mCornersListener = new TaskCornersListenerImpl();
+
     private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
             new SparseArray<>();
-    private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
+    private final DragListenerImpl mDragStartListener = new DragListenerImpl();
     private final InputMonitorFactory mInputMonitorFactory;
     private TaskOperations mTaskOperations;
     private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
@@ -779,6 +783,7 @@
         final DesktopModeTouchEventListener touchEventListener =
                 new DesktopModeTouchEventListener(taskInfo, taskPositioner);
         windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
+        windowDecoration.setCornersListener(mCornersListener);
         windowDecoration.setDragPositioningCallback(taskPositioner);
         windowDecoration.setDragDetector(touchEventListener.mDragDetector);
         windowDecoration.relayout(taskInfo, startT, finishT,
@@ -786,7 +791,7 @@
         incrementEventReceiverTasks(taskInfo.displayId);
     }
 
-    private class DragStartListenerImpl implements TaskPositioner.DragStartListener {
+    private class DragListenerImpl implements TaskPositioner.DragStartListener {
         @Override
         public void onDragStart(int taskId) {
             mWindowDecorByTaskId.get(taskId).closeHandleMenu();
@@ -798,6 +803,22 @@
             return inputManager.monitorGestureInput("caption-touch", context.getDisplayId());
         }
     }
+
+    private class TaskCornersListenerImpl
+            implements DesktopModeWindowDecoration.TaskCornersListener {
+
+        @Override
+        public void onTaskCornersChanged(int taskId, Region corner) {
+            mDesktopModeController.ifPresent(d -> d.onTaskCornersChanged(taskId, corner));
+            mDesktopTasksController.ifPresent(d -> d.onTaskCornersChanged(taskId, corner));
+        }
+
+        @Override
+        public void onTaskCornersRemoved(int taskId) {
+            mDesktopModeController.ifPresent(d -> d.removeCornersForTask(taskId));
+            mDesktopTasksController.ifPresent(d -> d.removeCornersForTask(taskId));
+        }
+    }
 }
 
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index af3fb0e..619e963 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -30,6 +30,7 @@
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
+import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.util.Log;
@@ -80,6 +81,7 @@
     private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
             new WindowDecoration.RelayoutResult<>();
 
+    private final Point mPositionInParent = new Point();
     private final PointF mHandleMenuAppInfoPillPosition = new PointF();
     private final PointF mHandleMenuWindowingPillPosition = new PointF();
     private final PointF mHandleMenuMoreActionsPillPosition = new PointF();
@@ -92,6 +94,8 @@
     private Drawable mAppIcon;
     private CharSequence mAppName;
 
+    private TaskCornersListener mCornersListener;
+
     private int mMenuWidth;
     private int mMarginMenuTop;
     private int mMarginMenuStart;
@@ -161,6 +165,10 @@
         mOnCaptionTouchListener = onCaptionTouchListener;
     }
 
+    void setCornersListener(TaskCornersListener cornersListener) {
+        mCornersListener = cornersListener;
+    }
+
     void setDragPositioningCallback(DragPositioningCallback dragPositioningCallback) {
         mDragPositioningCallback = dragPositioningCallback;
     }
@@ -278,8 +286,14 @@
                 .getDimensionPixelSize(R.dimen.freeform_resize_handle);
         final int resize_corner = mResult.mRootView.getResources()
                 .getDimensionPixelSize(R.dimen.freeform_resize_corner);
-        mDragResizeListener.setGeometry(
-                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
+
+        // If either task geometry or position have changed, update this task's cornersListener
+        if (mDragResizeListener.setGeometry(
+                mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop)
+                || !mTaskInfo.positionInParent.equals(mPositionInParent)) {
+            mCornersListener.onTaskCornersChanged(mTaskInfo.taskId, getGlobalCornersRegion());
+        }
+        mPositionInParent.set(mTaskInfo.positionInParent);
     }
 
     boolean isHandleMenuActive() {
@@ -586,6 +600,7 @@
     public void close() {
         closeDragResizeListener();
         closeHandleMenu();
+        mCornersListener.onTaskCornersRemoved(mTaskInfo.taskId);
         super.close();
     }
 
@@ -598,6 +613,15 @@
                 : R.layout.desktop_mode_focused_window_decor;
     }
 
+    /**
+     * Create a new region out of the corner rects of this task.
+     */
+    Region getGlobalCornersRegion() {
+        Region cornersRegion = mDragResizeListener.getCornersRegion();
+        cornersRegion.translate(mPositionInParent.x, mPositionInParent.y);
+        return cornersRegion;
+    }
+
     static class Factory {
 
         DesktopModeWindowDecoration create(
@@ -620,4 +644,13 @@
                     syncQueue);
         }
     }
+
+    interface TaskCornersListener {
+        /** Inform the implementing class of this task's change in corner resize handles */
+        void onTaskCornersChanged(int taskId, Region corner);
+
+        /** Inform the implementing class that this task no longer needs its corners tracked,
+         * likely due to it closing. */
+        void onTaskCornersRemoved(int taskId);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index d5437c7..34c8c08 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -134,13 +134,14 @@
      * @param cornerSize The size of the resize handle centered in each corner.
      * @param touchSlop The distance in pixels user has to drag with touch for it to register as
      *                  a resize action.
+     * @return whether the geometry has changed or not
      */
-    void setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize,
+    boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize,
             int touchSlop) {
         if (mTaskWidth == taskWidth && mTaskHeight == taskHeight
                 && mResizeHandleThickness == resizeHandleThickness
                 && mCornerSize == cornerSize) {
-            return;
+            return false;
         }
 
         mTaskWidth = taskWidth;
@@ -220,6 +221,19 @@
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
+        return true;
+    }
+
+    /**
+     * Generate a Region that encapsulates all 4 corner handles
+     */
+    Region getCornersRegion() {
+        Region region = new Region();
+        region.union(mLeftTopCornerBounds);
+        region.union(mLeftBottomCornerBounds);
+        region.union(mRightTopCornerBounds);
+        region.union(mRightBottomCornerBounds);
+        return region;
     }
 
     @Override
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/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/activity_confirmation.xml b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
index d470d4c..aae30df 100644
--- a/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/activity_confirmation.xml
@@ -16,13 +16,15 @@
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        style="@style/ScrollViewStyle">
+        style="@style/ScrollViewStyle"
+        android:importantForAccessibility="no">
 
     <LinearLayout
             android:id="@+id/activity_confirmation"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:baselineAligned="false">
+            android:baselineAligned="false"
+            android:importantForAccessibility="no">
 
         <LinearLayout android:id="@+id/association_confirmation"
                       style="@style/ContainerLayout">
@@ -153,7 +155,8 @@
         <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_weight="1">
+            android:layout_weight="1"
+            android:importantForAccessibility="noHideDescendants">
 
             <ProgressBar
                 android:id="@+id/spinner_single_device"
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/layout/vendor_header.xml b/packages/CompanionDeviceManager/res/layout/vendor_header.xml
index 16a87e5..77c79b3 100644
--- a/packages/CompanionDeviceManager/res/layout/vendor_header.xml
+++ b/packages/CompanionDeviceManager/res/layout/vendor_header.xml
@@ -33,7 +33,7 @@
         android:layout_width="48dp"
         android:layout_height="48dp"
         android:padding="12dp"
-        android:contentDescription="@string/vendor_icon_description" />
+        android:importantForAccessibility="no" />
 
     <TextView
         android:id="@+id/vendor_header_name"
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index ebfb86d..74072e9 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>
@@ -120,11 +132,8 @@
     <!-- Text of the permission sync explanation in the confirmation dialog. [CHAR LIMIT=NONE] -->
     <string name="permission_sync_summary">This may include &lt;strong&gt;Microphone&lt;/strong&gt;, &lt;strong&gt;Camera&lt;/strong&gt;, and &lt;strong&gt;Location access&lt;/strong&gt;, and other sensitive permissions on &lt;strong&gt;<xliff:g id="companion_device_name" example="My Watch">%1$s</xliff:g>&lt;/strong&gt;. &lt;br/&gt;&lt;br/&gt;You can change these permissions any time in your Settings on &lt;strong&gt;<xliff:g id="companion_device_name" example="My Watch">%1$s</xliff:g>&lt;/strong&gt;.</string>
 
-    <!--Description for vendor icon [CHAR LIMIT=30]-->
-    <string name="vendor_icon_description">App Icon</string>
-
     <!--Description for information icon [CHAR LIMIT=30]-->
-    <string name="vendor_header_button_description">More Information Button</string>
+    <string name="vendor_header_button_description">More Information</string>
 
     <!-- ================= Permissions ================= -->
 
diff --git a/packages/CompanionDeviceManager/res/values/styles.xml b/packages/CompanionDeviceManager/res/values/styles.xml
index b167377..e85190b 100644
--- a/packages/CompanionDeviceManager/res/values/styles.xml
+++ b/packages/CompanionDeviceManager/res/values/styles.xml
@@ -36,7 +36,7 @@
     </style>
 
     <style name="DescriptionTitle"
-           parent="@android:style/TextAppearance.DeviceDefault.Medium">
+           parent="@android:style/TextAppearance.DeviceDefault">
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:gravity">center</item>
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/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/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 6d375ac..48259e1 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -46,12 +46,16 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Locale;
+import java.util.Set;
 
 public class SettingsHelper {
     private static final String TAG = "SettingsHelper";
     private static final String SILENT_RINGTONE = "_silent";
     private static final String SETTINGS_REPLACED_KEY = "backup_skip_user_facing_data";
     private static final String SETTING_ORIGINAL_KEY_SUFFIX = "_original";
+    private static final String UNICODE_LOCALE_EXTENSION_FW = "fw";
+    private static final String UNICODE_LOCALE_EXTENSION_MU = "mu";
+    private static final String UNICODE_LOCALE_EXTENSION_NU = "nu";
     private static final float FLOAT_TOLERANCE = 0.01f;
 
     /** See frameworks/base/core/res/res/values/config.xml#config_longPressOnPowerBehavior **/
@@ -97,6 +101,25 @@
         sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
     }
 
+    private static final ArraySet<String> UNICODE_LOCALE_SUPPORTED_EXTENSIONS = new ArraySet<>();
+
+    /**
+     * Current supported extensions are fw (first day of week) and mu (temperature unit) extension.
+     * User can set these extensions in Settings app, and it will be appended to the locale,
+     * for example: zh-Hant-TW-u-fw-mon-mu-celsius. So after the factory reset, these extensions
+     * should be restored as well because they are set by users.
+     * We do not put the nu (numbering system) extension here because it is an Android supported
+     * extension and defined in some particular locales, for example:
+     * ar-Arab-MA-u-nu-arab and ar-Arab-YE-u-nu-latn. See
+     * <code>frameworks/base/core/res/res/values/locale_config.xml</code>
+     * The nu extension should not be appended to the current/restored locale after factory reset
+     * if the current/restored locale does not have it.
+     */
+    static {
+        UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_FW);
+        UNICODE_LOCALE_SUPPORTED_EXTENSIONS.add(UNICODE_LOCALE_EXTENSION_MU);
+    }
+
     private interface SettingsLookup {
         public String lookup(ContentResolver resolver, String name, int userHandle);
     }
@@ -500,20 +523,25 @@
             allLocales.put(toFullLocale(locale), locale);
         }
 
+        // After restoring to reset locales, need to get extensions from restored locale. Get the
+        // first restored locale to check its extension.
+        final Locale restoredLocale = restore.isEmpty()
+                ? Locale.ROOT
+                : restore.get(0);
         final ArrayList<Locale> filtered = new ArrayList<>(current.size());
         for (int i = 0; i < current.size(); i++) {
-            final Locale locale = current.get(i);
+            Locale locale = copyExtensionToTargetLocale(restoredLocale, current.get(i));
             allLocales.remove(toFullLocale(locale));
             filtered.add(locale);
         }
 
         for (int i = 0; i < restore.size(); i++) {
-            final Locale locale = allLocales.remove(toFullLocale(restore.get(i)));
-            if (locale != null) {
-                filtered.add(locale);
+            final Locale restoredLocaleWithExtension = copyExtensionToTargetLocale(restoredLocale,
+                    getFilteredLocale(restore.get(i), allLocales));
+            if (restoredLocaleWithExtension != null) {
+                filtered.add(restoredLocaleWithExtension);
             }
         }
-
         if (filtered.size() == current.size()) {
             return current;  // Nothing added to current locale list.
         }
@@ -521,6 +549,45 @@
         return new LocaleList(filtered.toArray(new Locale[filtered.size()]));
     }
 
+    private static Locale copyExtensionToTargetLocale(Locale restoredLocale,
+            Locale targetLocale) {
+        if (!restoredLocale.hasExtensions()) {
+            return targetLocale;
+        }
+
+        if (targetLocale == null) {
+            return null;
+        }
+
+        Locale.Builder builder = new Locale.Builder()
+                .setLocale(targetLocale);
+        Set<String> unicodeLocaleKeys = restoredLocale.getUnicodeLocaleKeys();
+        unicodeLocaleKeys.stream().forEach(key -> {
+            // Copy all supported extensions from restored locales except "nu" extension. The "nu"
+            // extension has been added in #getFilteredLocale(Locale, HashMap<Locale, Locale>)
+            // already, we don't need to add it again.
+            if (UNICODE_LOCALE_SUPPORTED_EXTENSIONS.contains(key)) {
+                builder.setUnicodeLocaleKeyword(key, restoredLocale.getUnicodeLocaleType(key));
+            }
+        });
+        return builder.build();
+    }
+
+    private static Locale getFilteredLocale(Locale restoreLocale,
+            HashMap<Locale, Locale> allLocales) {
+        Locale locale = allLocales.remove(toFullLocale(restoreLocale));
+        if (locale != null) {
+            return locale;
+        }
+
+        Locale filteredLocale = new Locale.Builder()
+                .setLocale(restoreLocale.stripExtensions())
+                .setUnicodeLocaleKeyword(UNICODE_LOCALE_EXTENSION_NU,
+                        restoreLocale.getUnicodeLocaleType(UNICODE_LOCALE_EXTENSION_NU))
+                .build();
+        return allLocales.remove(toFullLocale(filteredLocale));
+    }
+
     /**
      * Sets the locale specified. Input data is the byte representation of comma separated
      * multiple BCP-47 language tags. For backwards compatibility, strings of the form
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index ee76dbf..bc81c44 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -299,12 +299,42 @@
                         LocaleList.forLanguageTags("en-US"),  // current
                         new String[] { "en-US", "zh-Hans-CN" }));  // supported
 
-        // Old langauge code should be updated.
+        // Old language code should be updated.
         assertEquals(LocaleList.forLanguageTags("en-US,he-IL,id-ID,yi"),
                 SettingsHelper.resolveLocales(
                         LocaleList.forLanguageTags("iw-IL,in-ID,ji"),  // restore
                         LocaleList.forLanguageTags("en-US"),  // current
                         new String[] { "he-IL", "id-ID", "yi" }));  // supported
+
+        // No matter the current locale has "nu" extension or not, if the restored locale has fw
+        // (first day of week) or mu(temperature unit) extension, we should restore fw or mu
+        // extensions as well and append these to restore and current locales.
+        assertEquals(LocaleList.forLanguageTags(
+                "en-US-u-fw-mon-mu-celsius,zh-Hant-TW-u-fw-mon-mu-celsius"),
+                SettingsHelper.resolveLocales(
+                        LocaleList.forLanguageTags("zh-Hant-TW-u-fw-mon-mu-celsius"),  // restore
+                        LocaleList.forLanguageTags("en-US"),  // current
+                        new String[] { "en-US", "zh-Hant-TW" }));  // supported
+
+        // No matter the current locale has "nu" extension or not, if the restored locale has fw
+        // (first day of week) or mu(temperature unit) extension, we should restore fw or mu
+        // extensions as well and append these to restore and current locales.
+        assertEquals(LocaleList.forLanguageTags(
+                "fa-Arab-AF-u-nu-latn-fw-mon-mu-celsius,zh-Hant-TW-u-fw-mon-mu-celsius"),
+                SettingsHelper.resolveLocales(
+                        LocaleList.forLanguageTags("zh-Hant-TW-u-fw-mon-mu-celsius"),  // restore
+                        LocaleList.forLanguageTags("fa-Arab-AF-u-nu-latn"),  // current
+                        new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" }));  // supported
+
+        // If the restored locale only has nu extension, we should not restore the nu extensions to
+        // current locales.
+        assertEquals(LocaleList.forLanguageTags("zh-Hant-TW,fa-Arab-AF-u-nu-latn"),
+                SettingsHelper.resolveLocales(
+                        LocaleList.forLanguageTags("fa-Arab-AF-u-nu-latn"),  // restore
+                        LocaleList.forLanguageTags("zh-Hant-TW"),  // current
+                        new String[] { "fa-Arab-AF-u-nu-latn", "zh-Hant-TW" }));  // supported
+
+
     }
 
     @Test
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 62c6c1d..865b0df 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -173,6 +173,7 @@
         "androidx.palette_palette",
         "androidx.legacy_legacy-preference-v14",
         "androidx.leanback_leanback",
+        "androidx.tracing_tracing",
         "androidx.slice_slice-core",
         "androidx.slice_slice-view",
         "androidx.slice_slice-builders",
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/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 387b67d..520c888 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -40,7 +40,8 @@
                 SoftwareBitmapDetector.ISSUE,
                 NonInjectedServiceDetector.ISSUE,
                 StaticSettingsProviderDetector.ISSUE,
-                DemotingTestWithoutBugDetector.ISSUE
+                DemotingTestWithoutBugDetector.ISSUE,
+                TestFunctionNameViolationDetector.ISSUE,
             )
 
     override val api: Int
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/TestFunctionNameViolationDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/TestFunctionNameViolationDetector.kt
new file mode 100644
index 0000000..d91c7e5
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/TestFunctionNameViolationDetector.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.AnnotationInfo
+import com.android.tools.lint.detector.api.AnnotationUsageInfo
+import com.android.tools.lint.detector.api.AnnotationUsageType
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.kotlin.KotlinUAnnotation
+import org.jetbrains.uast.kotlin.KotlinUMethod
+
+/**
+ * Detects test function naming violations regarding use of the backtick-wrapped space-allowed
+ * feature of Kotlin functions.
+ */
+class TestFunctionNameViolationDetector : Detector(), SourceCodeScanner {
+
+    override fun applicableAnnotations(): List<String> = listOf(ANNOTATION)
+    override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean = true
+
+    @Suppress("UnstableApiUsage")
+    override fun visitAnnotationUsage(
+        context: JavaContext,
+        element: UElement,
+        annotationInfo: AnnotationInfo,
+        usageInfo: AnnotationUsageInfo,
+    ) {
+        (element as? KotlinUAnnotation)?.getParentOfType(KotlinUMethod::class.java)?.let { method ->
+            if (method.name.contains(" ")) {
+                context.report(
+                    issue = ISSUE,
+                    scope = method.nameIdentifier,
+                    location = context.getLocation(method.nameIdentifier),
+                    message =
+                        "Spaces are not allowed in test names. Use pascalCase_withUnderScores" +
+                            " instead.",
+                )
+            }
+        }
+    }
+
+    companion object {
+        private const val ANNOTATION = "org.junit.Test"
+
+        @JvmStatic
+        val ISSUE =
+            Issue.create(
+                id = "TestFunctionNameViolation",
+                briefDescription = "Spaces not allowed in test function names.",
+                explanation =
+                    """
+                    We don't allow test function names because it leads to issues with our test
+                    harness system (for example, see b/277739595). Please use
+                    pascalCase_withUnderScores instead.
+                """,
+                category = Category.TESTING,
+                priority = 8,
+                severity = Severity.FATAL,
+                implementation =
+                    Implementation(
+                        TestFunctionNameViolationDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE,
+                    ),
+            )
+    }
+}
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/TestFunctionNameViolationDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/TestFunctionNameViolationDetectorTest.kt
new file mode 100644
index 0000000..db73154
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/TestFunctionNameViolationDetectorTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+@Suppress("UnstableApiUsage")
+class TestFunctionNameViolationDetectorTest : SystemUILintDetectorTest() {
+    override fun getDetector(): Detector {
+        return TestFunctionNameViolationDetector()
+    }
+
+    override fun getIssues(): List<Issue> {
+        return listOf(
+            TestFunctionNameViolationDetector.ISSUE,
+        )
+    }
+
+    @Test
+    fun violations() {
+        lint()
+            .files(
+                kotlin(
+                    """
+                    package test.pkg.name
+
+                    import org.junit.Test
+
+                    class MyTest {
+                        @Test
+                        fun `illegal test name - violation should be detected`() {
+                            // some test code here.
+                        }
+
+                        @Test
+                        fun legitimateTestName_doesNotViolate() {
+                            // some test code here.
+                        }
+
+                        fun helperFunction_doesNotViolate() {
+                            // some code.
+                        }
+
+                        fun `helper function - does not violate`() {
+                            // some code.
+                        }
+                    }
+                """
+                        .trimIndent()
+                ),
+                testAnnotationStub,
+            )
+            .issues(
+                TestFunctionNameViolationDetector.ISSUE,
+            )
+            .run()
+            .expectWarningCount(0)
+            .expect(
+                """
+                src/test/pkg/name/MyTest.kt:7: Error: Spaces are not allowed in test names. Use pascalCase_withUnderScores instead. [TestFunctionNameViolation]
+                    fun `illegal test name - violation should be detected`() {
+                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                1 errors, 0 warnings
+                """
+            )
+    }
+
+    companion object {
+        private val testAnnotationStub: TestFile =
+            kotlin(
+                """
+                package org.junit
+
+                import java.lang.annotation.ElementType
+                import java.lang.annotation.Retention
+                import java.lang.annotation.RetentionPolicy
+                import java.lang.annotation.Target
+
+                @Retention(RetentionPolicy.RUNTIME)
+                @Target({ElementType.METHOD})
+                annotation class Test
+            """
+            )
+    }
+}
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/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/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 6e8275f..7bfd84e 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -346,6 +346,15 @@
             setSystemGestureExclusion();
             mIsVisible = true;
             mCallback.onSettingsPanelVisibilityChanged(/* shown= */ true);
+
+            if (resetPosition) {
+                // We could not put focus on the settings panel automatically
+                // since it is an inactive window. Therefore, we announce the existence of
+                // magnification settings for accessibility when it is opened.
+                mSettingView.announceForAccessibility(
+                        mContext.getResources().getString(
+                                R.string.accessibility_magnification_settings_panel_description));
+            }
         }
         mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index e0b9f01..d0ac296 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -23,14 +23,17 @@
 import android.graphics.Point
 import android.hardware.biometrics.BiometricFingerprintConstants
 import android.hardware.biometrics.BiometricSourceType
+import android.util.DisplayMetrics
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.keyguard.logging.KeyguardLogger
 import com.android.settingslib.Utils
 import com.android.settingslib.udfps.UdfpsOverlayParams
+import com.android.systemui.CoreStartable
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.WakefulnessLifecycle
@@ -39,12 +42,11 @@
 import com.android.systemui.statusbar.CircleReveal
 import com.android.systemui.statusbar.LiftReveal
 import com.android.systemui.statusbar.LightRevealEffect
+import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.commandline.Command
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.phone.BiometricUnlockController
-import com.android.systemui.statusbar.phone.CentralSurfaces
-import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.ViewController
@@ -60,9 +62,8 @@
  *
  * The ripple uses the accent color of the current theme.
  */
-@CentralSurfacesScope
+@SysUISingleton
 class AuthRippleController @Inject constructor(
-    private val centralSurfaces: CentralSurfaces,
     private val sysuiContext: Context,
     private val authController: AuthController,
     private val configurationController: ConfigurationController,
@@ -73,12 +74,15 @@
     private val notificationShadeWindowController: NotificationShadeWindowController,
     private val udfpsControllerProvider: Provider<UdfpsController>,
     private val statusBarStateController: StatusBarStateController,
+    private val displayMetrics: DisplayMetrics,
     private val featureFlags: FeatureFlags,
     private val logger: KeyguardLogger,
     private val biometricUnlockController: BiometricUnlockController,
+    private val lightRevealScrim: LightRevealScrim,
     rippleView: AuthRippleView?
 ) :
     ViewController<AuthRippleView>(rippleView),
+    CoreStartable,
     KeyguardStateController.Callback,
     WakefulnessLifecycle.Observer {
 
@@ -92,6 +96,10 @@
     private var udfpsController: UdfpsController? = null
     private var udfpsRadius: Float = -1f
 
+    override fun start() {
+        init()
+    }
+
     @VisibleForTesting
     public override fun onViewAttached() {
         authController.addCallback(authControllerCallback)
@@ -153,8 +161,8 @@
                         it.y,
                         0,
                         Math.max(
-                                Math.max(it.x, centralSurfaces.displayWidth.toInt() - it.x),
-                                Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y)
+                                Math.max(it.x, displayMetrics.widthPixels - it.x),
+                                Math.max(it.y, displayMetrics.heightPixels - it.y)
                         )
                 )
                 logger.showingUnlockRippleAt(it.x, it.y, "FP sensor radius: $udfpsRadius")
@@ -168,8 +176,8 @@
                         it.y,
                         0,
                         Math.max(
-                                Math.max(it.x, centralSurfaces.displayWidth.toInt() - it.x),
-                                Math.max(it.y, centralSurfaces.displayHeight.toInt() - it.y)
+                                Math.max(it.x, displayMetrics.widthPixels - it.x),
+                                Math.max(it.y, displayMetrics.heightPixels - it.y)
                         )
                 )
                 logger.showingUnlockRippleAt(it.x, it.y, "Face unlock ripple")
@@ -184,11 +192,10 @@
         // This code path is not used if the KeyguardTransitionRepository is managing the light
         // reveal scrim.
         if (!featureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) {
-            val lightRevealScrim = centralSurfaces.lightRevealScrim
             if (statusBarStateController.isDozing || biometricUnlockController.isWakeAndUnlock) {
                 circleReveal?.let {
-                    lightRevealScrim?.revealAmount = 0f
-                    lightRevealScrim?.revealEffect = it
+                    lightRevealScrim.revealAmount = 0f
+                    lightRevealScrim.revealEffect = it
                     startLightRevealScrimOnKeyguardFadingAway = true
                 }
             }
@@ -208,8 +215,7 @@
         }
 
         if (keyguardStateController.isKeyguardFadingAway) {
-            val lightRevealScrim = centralSurfaces.lightRevealScrim
-            if (startLightRevealScrimOnKeyguardFadingAway && lightRevealScrim != null) {
+            if (startLightRevealScrimOnKeyguardFadingAway) {
                 lightRevealScrimAnimator?.cancel()
                 lightRevealScrimAnimator = ValueAnimator.ofFloat(.1f, 1f).apply {
                     interpolator = Interpolators.LINEAR_OUT_SLOW_IN
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 0be3bb6..0dcba50 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -50,6 +50,7 @@
 import android.content.pm.ShortcutManager;
 import android.content.res.AssetManager;
 import android.content.res.Resources;
+import android.graphics.Color;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricManager;
@@ -113,13 +114,13 @@
 import com.android.systemui.dagger.qualifiers.TestHarness;
 import com.android.systemui.shared.system.PackageManagerWrapper;
 
+import dagger.Module;
+import dagger.Provides;
+
 import java.util.Optional;
 
 import javax.inject.Singleton;
 
-import dagger.Module;
-import dagger.Provides;
-
 /**
  * Provides Non-SystemUI, Framework-Owned instances to the dependency graph.
  */
@@ -323,7 +324,9 @@
     @Provides
     @Singleton
     static InteractionJankMonitor provideInteractionJankMonitor() {
-        return InteractionJankMonitor.getInstance();
+        InteractionJankMonitor jankMonitor = InteractionJankMonitor.getInstance();
+        jankMonitor.configDebugOverlay(Color.YELLOW, 0.75);
+        return jankMonitor;
     }
 
     @Provides
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/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index b9ef916..41e3e6d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -89,6 +89,7 @@
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
 import com.android.systemui.util.Assert;
 import com.android.wm.shell.back.BackAnimation;
+import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.pip.Pip;
 
 import java.io.PrintWriter;
@@ -190,6 +191,7 @@
     private final WindowManager mWindowManager;
     private final IWindowManager mWindowManagerService;
     private final Optional<Pip> mPipOptional;
+    private final Optional<DesktopMode> mDesktopModeOptional;
     private final FalsingManager mFalsingManager;
     private final Configuration mLastReportedConfig = new Configuration();
     // Activities which should not trigger Back gesture.
@@ -204,6 +206,7 @@
     private final Rect mPipExcludedBounds = new Rect();
     private final Rect mNavBarOverlayExcludedBounds = new Rect();
     private final Region mExcludeRegion = new Region();
+    private final Region mDesktopModeExcludeRegion = new Region();
     private final Region mUnrestrictedExcludeRegion = new Region();
     private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
     private final Provider<BackGestureTfClassifierProvider>
@@ -328,6 +331,9 @@
     private final Consumer<Boolean> mOnIsInPipStateChangedListener =
             (isInPip) -> mIsInPip = isInPip;
 
+    private final Consumer<Region> mDesktopCornersChangedListener =
+            (desktopExcludeRegion) -> mDesktopModeExcludeRegion.set(desktopExcludeRegion);
+
     private final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
                 @Override
@@ -352,6 +358,7 @@
             WindowManager windowManager,
             IWindowManager windowManagerService,
             Optional<Pip> pipOptional,
+            Optional<DesktopMode> desktopModeOptional,
             FalsingManager falsingManager,
             Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider,
             Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
@@ -372,6 +379,7 @@
         mWindowManager = windowManager;
         mWindowManagerService = windowManagerService;
         mPipOptional = pipOptional;
+        mDesktopModeOptional = desktopModeOptional;
         mFalsingManager = falsingManager;
         mNavBarEdgePanelProvider = navigationBarEdgePanelProvider;
         mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
@@ -580,6 +588,9 @@
                     mMainExecutor::execute, mOnPropertiesChangedListener);
             mPipOptional.ifPresent(
                     pip -> pip.setOnIsInPipStateChangedListener(mOnIsInPipStateChangedListener));
+            mDesktopModeOptional.ifPresent(
+                    dm -> dm.addDesktopGestureExclusionRegionListener(
+                            mDesktopCornersChangedListener, mMainExecutor));
 
             try {
                 mWindowManagerService.registerSystemGestureExclusionListener(
@@ -802,11 +813,17 @@
                 mDisplaySize.y - insets.bottom);
     }
 
+    private boolean desktopExcludeRegionContains(int x, int y) {
+        return mDesktopModeExcludeRegion.contains(x, y);
+    }
+
     private boolean isWithinTouchRegion(int x, int y) {
         // If the point is inside the PiP or Nav bar overlay excluded bounds, then ignore the back
         // gesture
         final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y);
-        if (isInsidePip || mNavBarOverlayExcludedBounds.contains(x, y)) {
+        final boolean isInDesktopExcludeRegion = desktopExcludeRegionContains(x, y);
+        if (isInsidePip || isInDesktopExcludeRegion
+                || mNavBarOverlayExcludedBounds.contains(x, y)) {
             return false;
         }
 
@@ -1136,6 +1153,7 @@
         pw.println("  mUnrestrictedExcludeRegion=" + mUnrestrictedExcludeRegion);
         pw.println("  mIsInPip=" + mIsInPip);
         pw.println("  mPipExcludedBounds=" + mPipExcludedBounds);
+        pw.println("  mDesktopModeExclusionRegion=" + mDesktopModeExcludeRegion);
         pw.println("  mNavBarOverlayExcludedBounds=" + mNavBarOverlayExcludedBounds);
         pw.println("  mEdgeWidthLeft=" + mEdgeWidthLeft);
         pw.println("  mEdgeWidthRight=" + mEdgeWidthRight);
@@ -1206,6 +1224,7 @@
         private final WindowManager mWindowManager;
         private final IWindowManager mWindowManagerService;
         private final Optional<Pip> mPipOptional;
+        private final Optional<DesktopMode> mDesktopModeOptional;
         private final FalsingManager mFalsingManager;
         private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
         private final Provider<BackGestureTfClassifierProvider>
@@ -1227,6 +1246,7 @@
                        WindowManager windowManager,
                        IWindowManager windowManagerService,
                        Optional<Pip> pipOptional,
+                       Optional<DesktopMode> desktopModeOptional,
                        FalsingManager falsingManager,
                        Provider<NavigationBarEdgePanel> navBarEdgePanelProvider,
                        Provider<BackGestureTfClassifierProvider>
@@ -1246,6 +1266,7 @@
             mWindowManager = windowManager;
             mWindowManagerService = windowManagerService;
             mPipOptional = pipOptional;
+            mDesktopModeOptional = desktopModeOptional;
             mFalsingManager = falsingManager;
             mNavBarEdgePanelProvider = navBarEdgePanelProvider;
             mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
@@ -1270,6 +1291,7 @@
                     mWindowManager,
                     mWindowManagerService,
                     mPipOptional,
+                    mDesktopModeOptional,
                     mFalsingManager,
                     mNavBarEdgePanelProvider,
                     mBackGestureTfClassifierProviderProvider,
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/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 0641eec..a3b901b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -44,6 +44,7 @@
 import android.widget.TextView
 import androidx.annotation.GuardedBy
 import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
 import androidx.recyclerview.widget.DiffUtil
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -201,7 +202,7 @@
     @GuardedBy("lock")
     private val appListAdapter: AppListAdapter = AppListAdapter()
 
-    @GuardedBy("lock")
+    /* Only mutate on the background thread */
     private var runningApps: ArrayMap<UserPackage, RunningApp> = ArrayMap()
 
     private val userTrackerCallback = object : UserTracker.Callback {
@@ -374,11 +375,6 @@
     override fun showDialog(expandable: Expandable?) {
         synchronized(lock) {
             if (dialog == null) {
-
-                runningTaskIdentifiers.keys.forEach {
-                    it.updateUiControl()
-                }
-
                 val dialog = SystemUIDialog(context)
                 dialog.setTitle(R.string.fgs_manager_dialog_title)
                 dialog.setMessage(R.string.fgs_manager_dialog_message)
@@ -421,33 +417,53 @@
                     }
                 }
 
-                backgroundExecutor.execute {
-                    synchronized(lock) {
-                        updateAppItemsLocked()
-                    }
-                }
+                updateAppItemsLocked(refreshUiControls = true)
             }
         }
     }
 
     @GuardedBy("lock")
-    private fun updateAppItemsLocked() {
+    private fun updateAppItemsLocked(refreshUiControls: Boolean = false) {
         if (dialog == null) {
-            runningApps.clear()
+            backgroundExecutor.execute {
+                clearRunningApps()
+            }
             return
         }
 
-        val addedPackages = runningTaskIdentifiers.keys.filter {
-            currentProfileIds.contains(it.userId) &&
+        val packagesToStartTime = runningTaskIdentifiers.mapValues { it.value.startTime }
+        val profileIds = currentProfileIds.toSet()
+        backgroundExecutor.execute {
+            updateAppItems(packagesToStartTime, profileIds, refreshUiControls)
+        }
+    }
+
+    /**
+     * Must be called on the background thread.
+     */
+    @WorkerThread
+    private fun updateAppItems(
+        packages: Map<UserPackage, Long>,
+        profileIds: Set<Int>,
+        refreshUiControls: Boolean = true
+    ) {
+        if (refreshUiControls) {
+            packages.forEach { (pkg, _) ->
+                pkg.updateUiControl()
+            }
+        }
+
+        val addedPackages = packages.keys.filter {
+            profileIds.contains(it.userId) &&
                     it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true
         }
-        val removedPackages = runningApps.keys.filter { !runningTaskIdentifiers.containsKey(it) }
+        val removedPackages = runningApps.keys.filter { it !in packages }
 
         addedPackages.forEach {
             val ai = packageManager.getApplicationInfoAsUser(it.packageName, 0, it.userId)
             runningApps[it] = RunningApp(
                 it.userId, it.packageName,
-                runningTaskIdentifiers[it]!!.startTime, it.uiControl,
+                packages[it]!!, it.uiControl,
                 packageManager.getApplicationLabel(ai),
                 packageManager.getUserBadgedIcon(
                     packageManager.getApplicationIcon(ai), UserHandle.of(it.userId)
@@ -472,6 +488,14 @@
         }
     }
 
+    /**
+     * Must be called on the background thread.
+     */
+    @WorkerThread
+    private fun clearRunningApps() {
+        runningApps.clear()
+    }
+
     private fun stopPackage(userId: Int, packageName: String, timeStarted: Long) {
         logEvent(stopped = true, packageName, userId, timeStarted)
         val userPackageKey = UserPackage(userId, packageName)
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/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index b0b9ab2..4c6673c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -17,15 +17,28 @@
 package com.android.systemui.shade
 
 import android.view.LayoutInflater
+import com.android.systemui.CoreStartable
 import com.android.systemui.R
+import com.android.systemui.biometrics.AuthRippleController
+import com.android.systemui.biometrics.AuthRippleView
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.LightRevealScrim
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 
 /** Module for classes related to the notification shade. */
 @Module
 abstract class ShadeModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(AuthRippleController::class)
+    abstract fun bindAuthRippleController(controller: AuthRippleController): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
@@ -59,5 +72,22 @@
         ): NotificationPanelView {
             return notificationShadeWindowView.findViewById(R.id.notification_panel)
         }
+
+        @Provides
+        @SysUISingleton
+        fun providesLightRevealScrim(
+            notificationShadeWindowView: NotificationShadeWindowView,
+        ): LightRevealScrim {
+            return notificationShadeWindowView.findViewById(R.id.light_reveal_scrim)
+        }
+
+        // TODO(b/277762009): Only allow this view's controller to inject the view. See above.
+        @Provides
+        @SysUISingleton
+        fun providesAuthRippleView(
+            notificationShadeWindowView: NotificationShadeWindowView,
+        ): AuthRippleView? {
+            return notificationShadeWindowView.findViewById(R.id.auth_ripple)
+        }
     }
 }
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/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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index e66a8b1..5654772 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -452,7 +452,7 @@
     protected PhoneStatusBarView mStatusBarView;
     private PhoneStatusBarViewController mPhoneStatusBarViewController;
     private PhoneStatusBarTransitions mStatusBarTransitions;
-    private AuthRippleController mAuthRippleController;
+    private final AuthRippleController mAuthRippleController;
     @WindowVisibleState private int mStatusBarWindowState = WINDOW_STATE_SHOWING;
     protected final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarInitializer mStatusBarInitializer;
@@ -460,7 +460,7 @@
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @VisibleForTesting
     DozeServiceHost mDozeServiceHost;
-    private LightRevealScrim mLightRevealScrim;
+    private final LightRevealScrim mLightRevealScrim;
     private PowerButtonReveal mPowerButtonReveal;
 
     private boolean mWakeUpComingFromTouch;
@@ -768,6 +768,7 @@
             ScrimController scrimController,
             Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
             Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+            AuthRippleController authRippleController,
             DozeServiceHost dozeServiceHost,
             PowerManager powerManager,
             ScreenPinningRequest screenPinningRequest,
@@ -810,6 +811,7 @@
             IDreamManager dreamManager,
             Lazy<CameraLauncher> cameraLauncherLazy,
             Lazy<LightRevealScrimViewModel> lightRevealScrimViewModelLazy,
+            LightRevealScrim lightRevealScrim,
             AlternateBouncerInteractor alternateBouncerInteractor,
             UserTracker userTracker,
             Provider<FingerprintManager> fingerprintManager
@@ -866,6 +868,7 @@
         mScreenPinningRequest = screenPinningRequest;
         mDozeScrimController = dozeScrimController;
         mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
+        mAuthRippleController = authRippleController;
         mNotificationShadeDepthControllerLazy = notificationShadeDepthControllerLazy;
         mVolumeComponent = volumeComponent;
         mCommandQueue = commandQueue;
@@ -932,6 +935,7 @@
         wiredChargingRippleController.registerCallbacks();
 
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
+        mLightRevealScrim = lightRevealScrim;
 
         // Based on teamfood flag, turn predictive back dispatch on at runtime.
         if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) {
@@ -1326,8 +1330,6 @@
         });
         mScrimController.attachViews(scrimBehind, notificationsScrim, scrimInFront);
 
-        mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
-
         if (mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION)) {
             LightRevealScrimViewBinder.bind(
                     mLightRevealScrim, mLightRevealScrimViewModelLazy.get());
@@ -1667,8 +1669,6 @@
         mPresenter = mCentralSurfacesComponent.getNotificationPresenter();
         mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter();
         mNotificationShelfController = mCentralSurfacesComponent.getNotificationShelfController();
-        mAuthRippleController = mCentralSurfacesComponent.getAuthRippleController();
-        mAuthRippleController.init();
 
         mHeadsUpManager.addListener(mCentralSurfacesComponent.getStatusBarHeadsUpChangeListener());
 
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/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
index d80e1d3..b16d16a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java
@@ -122,11 +122,6 @@
     LockIconViewController getLockIconViewController();
 
     /**
-     * Creates an AuthRippleViewController. Must be init after creation.
-     */
-    AuthRippleController getAuthRippleController();
-
-    /**
      * Creates a StatusBarHeadsUpChangeListener.
      */
     StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener();
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..1a943e7 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
@@ -29,7 +29,6 @@
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.battery.BatteryMeterViewController;
-import com.android.systemui.biometrics.AuthRippleView;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
@@ -55,6 +54,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 +87,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 +120,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)
@@ -148,15 +149,6 @@
 
     /** */
     @Provides
-    @CentralSurfacesComponent.CentralSurfacesScope
-    @Nullable
-    public static AuthRippleView getAuthRippleView(
-            NotificationShadeWindowView notificationShadeWindowView) {
-        return notificationShadeWindowView.findViewById(R.id.auth_ripple);
-    }
-
-    /** */
-    @Provides
     @Named(SHADE_HEADER)
     @CentralSurfacesComponent.CentralSurfacesScope
     public static MotionLayout getLargeScreenShadeHeaderBarView(
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/util/TraceUtils.kt b/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt
index 64234c2..41c6b5d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/TraceUtils.kt
@@ -16,20 +16,22 @@
 
 package com.android.systemui.util
 
-import android.os.Trace
+import android.os.Handler
 import android.os.TraceNameSupplier
+import androidx.tracing.Trace
 
 /**
- * Run a block within a [Trace] section.
- * Calls [Trace.beginSection] before and [Trace.endSection] after the passed block.
+ * Run a block within a [Trace] section. Calls [Trace.beginSection] before and [Trace.endSection]
+ * after the passed block. If tracing is disabled, it will run the block directly to avoid using an
+ * unnecessary try-finally block.
  */
 inline fun <T> traceSection(tag: String, block: () -> T): T =
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
-            Trace.traceBegin(Trace.TRACE_TAG_APP, tag)
+        if (Trace.isEnabled()) {
+            Trace.beginSection(tag)
             try {
                 block()
             } finally {
-                Trace.traceEnd(Trace.TRACE_TAG_APP)
+                Trace.endSection()
             }
         } else {
             block()
@@ -42,8 +44,10 @@
         }
 
         /**
-         * Helper function for creating a Runnable object that implements TraceNameSupplier.
-         * This is useful for posting Runnables to Handlers with meaningful names.
+         * Helper function for creating a [Runnable] that implements [TraceNameSupplier]. This is
+         * useful when posting to a [Handler] so that the [Runnable] has a meaningful name in the
+         * trace. Otherwise, the class name of the [Runnable] is used, which is often something like
+         * `pkg.MyClass$$ExternalSyntheticLambda0`.
          */
         inline fun namedRunnable(tag: String, crossinline block: () -> Unit): Runnable {
             return object : Runnable, TraceNameSupplier {
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/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index e492534..b3e7cb0 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -345,7 +345,8 @@
     }
 
     void initDesktopMode(DesktopMode desktopMode) {
-        desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() {
+        desktopMode.addVisibleTasksListener(
+                new DesktopModeTaskRepository.VisibleTasksListener() {
             @Override
             public void onVisibilityChanged(boolean hasFreeformTasks) {
                 mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
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/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
index 7d9ccb6..6b5679a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt
@@ -21,6 +21,7 @@
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
+import android.util.DisplayMetrics
 import androidx.test.filters.SmallTest
 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -35,7 +36,6 @@
 import com.android.systemui.statusbar.NotificationShadeWindowController
 import com.android.systemui.statusbar.commandline.CommandRegistry
 import com.android.systemui.statusbar.phone.BiometricUnlockController
-import com.android.systemui.statusbar.phone.CentralSurfaces
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.leak.RotationUtils
@@ -66,7 +66,6 @@
     private lateinit var staticMockSession: MockitoSession
 
     private lateinit var controller: AuthRippleController
-    @Mock private lateinit var mCentralSurfaces: CentralSurfaces
     @Mock private lateinit var rippleView: AuthRippleView
     @Mock private lateinit var commandRegistry: CommandRegistry
     @Mock private lateinit var configurationController: ConfigurationController
@@ -92,6 +91,8 @@
     @Mock
     private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal
 
+    private val displayMetrics = DisplayMetrics()
+
     @Captor
     private lateinit var biometricUnlockListener:
             ArgumentCaptor<BiometricUnlockController.BiometricUnlockEventsListener>
@@ -109,7 +110,6 @@
         `when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
 
         controller = AuthRippleController(
-            mCentralSurfaces,
             context,
             authController,
             configurationController,
@@ -120,13 +120,14 @@
             notificationShadeWindowController,
             udfpsControllerProvider,
             statusBarStateController,
+            displayMetrics,
             featureFlags,
             KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)),
             biometricUnlockController,
+            lightRevealScrim,
             rippleView,
         )
         controller.init()
-        `when`(mCentralSurfaces.lightRevealScrim).thenReturn(lightRevealScrim)
     }
 
     @After
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 0ae012c..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;
@@ -250,6 +255,34 @@
     }
 
     @Test
+    public void testUpdateEmptyShadeView_bouncerShowing_hideEmptyView() {
+        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
+        initController(/* viewIsAttached= */ true);
+
+        when(mCentralSurfaces.isBouncerShowing()).thenReturn(true);
+        setupShowEmptyShadeViewState(true);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ false,
+                /* areNotificationsHiddenInShade= */ false);
+    }
+
+    @Test
+    public void testUpdateEmptyShadeView_bouncerNotShowing_showEmptyView() {
+        when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
+        initController(/* viewIsAttached= */ true);
+
+        when(mCentralSurfaces.isBouncerShowing()).thenReturn(false);
+        setupShowEmptyShadeViewState(true);
+        reset(mNotificationStackScrollLayout);
+        mController.updateShowEmptyShadeView();
+        verify(mNotificationStackScrollLayout).updateEmptyShadeView(
+                /* visible= */ true,
+                /* areNotificationsHiddenInShade= */ false);
+    }
+
+    @Test
     public void testOnUserChange_verifySensitiveProfile() {
         when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
         initController(/* viewIsAttached= */ true);
@@ -430,6 +463,7 @@
                 mKeyguardBypassController,
                 mZenModeController,
                 mNotificationLockscreenUserManager,
+                Optional.<NotificationListViewModel>empty(),
                 mMetricsLogger,
                 mDumpManager,
                 new FalsingCollectorFake(),
@@ -453,6 +487,7 @@
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
+                mIconAreaController,
                 mFeatureFlags,
                 mNotificationTargetsHelper,
                 mSecureSettings,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 219e6a9..c83769d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -101,6 +101,7 @@
 import com.android.systemui.accessibility.floatingmenu.AccessibilityFloatingMenuController;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.biometrics.AuthRippleController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.charging.WiredChargingRippleController;
 import com.android.systemui.classifier.FalsingCollectorFake;
@@ -138,6 +139,7 @@
 import com.android.systemui.shade.ShadeLogger;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.LightRevealScrim;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -231,10 +233,12 @@
     @Mock private IStatusBarService mBarService;
     @Mock private IDreamManager mDreamManager;
     @Mock private LightRevealScrimViewModel mLightRevealScrimViewModel;
+    @Mock private LightRevealScrim mLightRevealScrim;
     @Mock private ScrimController mScrimController;
     @Mock private DozeScrimController mDozeScrimController;
     @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock private BiometricUnlockController mBiometricUnlockController;
+    @Mock private AuthRippleController mAuthRippleController;
     @Mock private NotificationListener mNotificationListener;
     @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
@@ -345,6 +349,7 @@
         mFeatureFlags.set(Flags.WM_SHADE_ALLOW_BACK_GESTURE, true);
         // For the Shade to animate during the Back gesture, we must enable the animation flag.
         mFeatureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true);
+        mFeatureFlags.set(Flags.LIGHT_REVEAL_MIGRATION, true);
 
         IThermalService thermalService = mock(IThermalService.class);
         mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService,
@@ -497,6 +502,7 @@
                 mScrimController,
                 mLockscreenWallpaperLazy,
                 mBiometricUnlockControllerLazy,
+                mAuthRippleController,
                 mDozeServiceHost,
                 mPowerManager, mScreenPinningRequest,
                 mDozeScrimController,
@@ -538,6 +544,7 @@
                 mDreamManager,
                 mCameraLauncherLazy,
                 () -> mLightRevealScrimViewModel,
+                mLightRevealScrim,
                 mAlternateBouncerInteractor,
                 mUserTracker,
                 () -> mFingerprintManager
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/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 8cae998..9de7a87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -128,7 +128,7 @@
     @Test
     public void initDesktopMode_registersListener() {
         mWMShell.initDesktopMode(mDesktopMode);
-        verify(mDesktopMode).addListener(
+        verify(mDesktopMode).addVisibleTasksListener(
                 any(DesktopModeTaskRepository.VisibleTasksListener.class),
                 any(Executor.class));
     }
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/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index efff112..d8fbd08 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -604,25 +604,21 @@
         }
 
         @Override
-        @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports")
         public void addOnTransportsChangedListener(IOnTransportsChangedListener listener) {
             mTransportManager.addListener(listener);
         }
 
         @Override
-        @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports")
         public void removeOnTransportsChangedListener(IOnTransportsChangedListener listener) {
             mTransportManager.removeListener(listener);
         }
 
         @Override
-        @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports")
         public void sendMessage(int messageType, byte[] data, int[] associationIds) {
             mTransportManager.sendMessage(messageType, data, associationIds);
         }
 
         @Override
-        @GuardedBy("CompanionDeviceManagerService.this.mTransportManager.mTransports")
         public void addOnMessageReceivedListener(int messageType,
                 IOnMessageReceivedListener listener) {
             mTransportManager.addListener(messageType, listener);
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
index 19d8b87..6f99d86 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/contextsync/CallMetadataSyncConnectionService.java
@@ -17,26 +17,38 @@
 package com.android.server.companion.datatransfer.contextsync;
 
 import android.content.ComponentName;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.telecom.Call;
+import android.telecom.Connection;
 import android.telecom.ConnectionService;
 import android.telecom.PhoneAccount;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
+import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.UUID;
 
 /** Service for Telecom to bind to when call metadata is synced between devices. */
 public class CallMetadataSyncConnectionService extends ConnectionService {
 
+    private static final String TAG = "CallMetadataSyncConnectionService";
+
+    private AudioManager mAudioManager;
     private TelecomManager mTelecomManager;
-    private final Map<String, PhoneAccountHandle> mPhoneAccountHandles = new HashMap<>();
+    private final Map<PhoneAccountHandleIdentifier, PhoneAccountHandle> mPhoneAccountHandles =
+            new HashMap<>();
 
     @Override
     public void onCreate() {
         super.onCreate();
+
+        mAudioManager = getSystemService(AudioManager.class);
         mTelecomManager = getSystemService(TelecomManager.class);
     }
 
@@ -44,34 +56,277 @@
      * 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);
-        }
+    private void registerPhoneAccount(int associationId, String appIdentifier,
+            String humanReadableAppName) {
+        final PhoneAccountHandleIdentifier phoneAccountHandleIdentifier =
+                new PhoneAccountHandleIdentifier(associationId, appIdentifier);
+        final PhoneAccount phoneAccount = createPhoneAccount(phoneAccountHandleIdentifier,
+                humanReadableAppName);
+        mTelecomManager.registerPhoneAccount(phoneAccount);
+        mTelecomManager.enablePhoneAccount(mPhoneAccountHandles.get(phoneAccountHandleIdentifier),
+                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));
+    private void unregisterPhoneAccount(int associationId, String appIdentifier) {
+        mTelecomManager.unregisterPhoneAccount(mPhoneAccountHandles.remove(
+                new PhoneAccountHandleIdentifier(associationId, appIdentifier)));
     }
 
     @VisibleForTesting
-    PhoneAccount createPhoneAccount(String packageName, String humanReadableAppName) {
-        if (mPhoneAccountHandles.containsKey(packageName)) {
+    PhoneAccount createPhoneAccount(PhoneAccountHandleIdentifier phoneAccountHandleIdentifier,
+            String humanReadableAppName) {
+        if (mPhoneAccountHandles.containsKey(phoneAccountHandleIdentifier)) {
             // Already exists!
             return null;
         }
         final PhoneAccountHandle handle = new PhoneAccountHandle(
                 new ComponentName(this, CallMetadataSyncConnectionService.class),
                 UUID.randomUUID().toString());
-        mPhoneAccountHandles.put(packageName, handle);
+        mPhoneAccountHandles.put(phoneAccountHandleIdentifier, handle);
         return new PhoneAccount.Builder(handle, humanReadableAppName)
                 .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER
                         | PhoneAccount.CAPABILITY_SELF_MANAGED).build();
     }
+
+    static final class PhoneAccountHandleIdentifier {
+        private final int mAssociationId;
+        private final String mAppIdentifier;
+
+        PhoneAccountHandleIdentifier(int associationId, String appIdentifier) {
+            mAssociationId = associationId;
+            mAppIdentifier = appIdentifier;
+        }
+
+        public int getAssociationId() {
+            return mAssociationId;
+        }
+
+        public String getAppIdentifier() {
+            return mAppIdentifier;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mAssociationId, mAppIdentifier);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof PhoneAccountHandleIdentifier) {
+                return ((PhoneAccountHandleIdentifier) other).getAssociationId() == mAssociationId
+                        && mAppIdentifier != null
+                        && mAppIdentifier.equals(
+                        ((PhoneAccountHandleIdentifier) other).getAppIdentifier());
+            }
+            return false;
+        }
+    }
+
+    private static final class CallMetadataSyncConnectionIdentifier {
+        private final int mAssociationId;
+        private final long mCallId;
+
+        CallMetadataSyncConnectionIdentifier(int associationId, long callId) {
+            mAssociationId = associationId;
+            mCallId = callId;
+        }
+
+        public int getAssociationId() {
+            return mAssociationId;
+        }
+
+        public long getCallId() {
+            return mCallId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mAssociationId, mCallId);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other instanceof CallMetadataSyncConnectionIdentifier) {
+                return ((CallMetadataSyncConnectionIdentifier) other).getAssociationId()
+                        == mAssociationId
+                        && (((CallMetadataSyncConnectionIdentifier) other).getCallId() == mCallId);
+            }
+            return false;
+        }
+    }
+
+    private abstract static class CallMetadataSyncConnectionCallback {
+
+        abstract void sendCallAction(int associationId, long callId, int action);
+
+        abstract void sendStateChange(int associationId, long callId, int newState);
+    }
+
+    private static class CallMetadataSyncConnection extends Connection {
+
+        private final TelecomManager mTelecomManager;
+        private final AudioManager mAudioManager;
+        private final int mAssociationId;
+        private final CallMetadataSyncData.Call mCall;
+        private final CallMetadataSyncConnectionCallback mCallback;
+
+        CallMetadataSyncConnection(TelecomManager telecomManager, AudioManager audioManager,
+                int associationId, CallMetadataSyncData.Call call,
+                CallMetadataSyncConnectionCallback callback) {
+            mTelecomManager = telecomManager;
+            mAudioManager = audioManager;
+            mAssociationId = associationId;
+            mCall = call;
+            mCallback = callback;
+        }
+
+        public long getCallId() {
+            return mCall.getId();
+        }
+
+        public void initialize() {
+            final int status = mCall.getStatus();
+            if (status == android.companion.Telecom.Call.RINGING_SILENCED) {
+                mTelecomManager.silenceRinger();
+            }
+            final int state = CrossDeviceCall.convertStatusToState(status);
+            if (state == Call.STATE_RINGING) {
+                setRinging();
+            } else if (state == Call.STATE_ACTIVE) {
+                setActive();
+            } else if (state == Call.STATE_HOLDING) {
+                setOnHold();
+            } else {
+                Slog.e(TAG, "Could not initialize call to unknown state");
+            }
+
+            final Bundle extras = new Bundle();
+            extras.putLong(CrossDeviceCall.EXTRA_CALL_ID, mCall.getId());
+            putExtras(extras);
+
+            int capabilities = getConnectionCapabilities();
+            if (mCall.hasControl(android.companion.Telecom.Call.PUT_ON_HOLD)) {
+                capabilities |= CAPABILITY_HOLD;
+            } else {
+                capabilities &= ~CAPABILITY_HOLD;
+            }
+            if (mCall.hasControl(android.companion.Telecom.Call.MUTE)) {
+                capabilities |= CAPABILITY_MUTE;
+            } else {
+                capabilities &= ~CAPABILITY_MUTE;
+            }
+            mAudioManager.setMicrophoneMute(
+                    mCall.hasControl(android.companion.Telecom.Call.UNMUTE));
+            if (capabilities != getConnectionCapabilities()) {
+                setConnectionCapabilities(capabilities);
+            }
+        }
+
+        public void update(CallMetadataSyncData.Call call) {
+            final int status = call.getStatus();
+            if (status == android.companion.Telecom.Call.RINGING_SILENCED
+                    && mCall.getStatus() != android.companion.Telecom.Call.RINGING_SILENCED) {
+                mTelecomManager.silenceRinger();
+            }
+            mCall.setStatus(status);
+            final int state = CrossDeviceCall.convertStatusToState(status);
+            if (state != getState()) {
+                if (state == Call.STATE_RINGING) {
+                    setRinging();
+                } else if (state == Call.STATE_ACTIVE) {
+                    setActive();
+                } else if (state == Call.STATE_HOLDING) {
+                    setOnHold();
+                } else {
+                    Slog.e(TAG, "Could not update call to unknown state");
+                }
+            }
+
+            int capabilities = getConnectionCapabilities();
+            final boolean hasHoldControl = mCall.hasControl(
+                    android.companion.Telecom.Call.PUT_ON_HOLD)
+                    || mCall.hasControl(android.companion.Telecom.Call.TAKE_OFF_HOLD);
+            if (hasHoldControl != ((getConnectionCapabilities() & CAPABILITY_HOLD)
+                    == CAPABILITY_HOLD)) {
+                if (hasHoldControl) {
+                    capabilities |= CAPABILITY_HOLD;
+                } else {
+                    capabilities &= ~CAPABILITY_HOLD;
+                }
+            }
+            final boolean hasMuteControl = mCall.hasControl(android.companion.Telecom.Call.MUTE);
+            if (hasMuteControl != ((getConnectionCapabilities() & CAPABILITY_MUTE)
+                    == CAPABILITY_MUTE)) {
+                if (hasMuteControl) {
+                    capabilities |= CAPABILITY_MUTE;
+                } else {
+                    capabilities &= ~CAPABILITY_MUTE;
+                }
+            }
+            mAudioManager.setMicrophoneMute(
+                    mCall.hasControl(android.companion.Telecom.Call.UNMUTE));
+            if (capabilities != getConnectionCapabilities()) {
+                setConnectionCapabilities(capabilities);
+            }
+        }
+
+        @Override
+        public void onAnswer(int videoState) {
+            sendCallAction(android.companion.Telecom.Call.ACCEPT);
+        }
+
+        @Override
+        public void onReject() {
+            sendCallAction(android.companion.Telecom.Call.REJECT);
+        }
+
+        @Override
+        public void onReject(int rejectReason) {
+            onReject();
+        }
+
+        @Override
+        public void onReject(String replyMessage) {
+            onReject();
+        }
+
+        @Override
+        public void onSilence() {
+            sendCallAction(android.companion.Telecom.Call.SILENCE);
+        }
+
+        @Override
+        public void onHold() {
+            sendCallAction(android.companion.Telecom.Call.PUT_ON_HOLD);
+        }
+
+        @Override
+        public void onUnhold() {
+            sendCallAction(android.companion.Telecom.Call.TAKE_OFF_HOLD);
+        }
+
+        @Override
+        public void onMuteStateChanged(boolean isMuted) {
+            sendCallAction(isMuted ? android.companion.Telecom.Call.MUTE
+                    : android.companion.Telecom.Call.UNMUTE);
+        }
+
+        @Override
+        public void onDisconnect() {
+            sendCallAction(android.companion.Telecom.Call.END);
+        }
+
+        @Override
+        public void onStateChanged(int state) {
+            mCallback.sendStateChange(mAssociationId, mCall.getId(), state);
+        }
+
+        private void sendCallAction(int action) {
+            mCallback.sendCallAction(mAssociationId, mCall.getId(), action);
+        }
+    }
 }
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/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 9677b70..a3e095e 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -96,27 +96,29 @@
     /**
      * Add a listener to receive callbacks when a message is received for the message type
      */
-    @GuardedBy("mTransports")
     public void addListener(int message, @NonNull IOnMessageReceivedListener listener) {
         mMessageListeners.put(message, listener);
-        for (int i = 0; i < mTransports.size(); i++) {
-            mTransports.valueAt(i).addListener(message, listener);
+        synchronized (mTransports) {
+            for (int i = 0; i < mTransports.size(); i++) {
+                mTransports.valueAt(i).addListener(message, listener);
+            }
         }
     }
 
     /**
      * Add a listener to receive callbacks when any of the transports is changed
      */
-    @GuardedBy("mTransports")
     public void addListener(IOnTransportsChangedListener listener) {
         Slog.i(TAG, "Registering OnTransportsChangedListener");
         mTransportsListeners.register(listener);
         List<AssociationInfo> associations = new ArrayList<>();
-        for (int i = 0; i < mTransports.size(); i++) {
-            AssociationInfo association = mAssociationStore.getAssociationById(
-                    mTransports.keyAt(i));
-            if (association != null) {
-                associations.add(association);
+        synchronized (mTransports) {
+            for (int i = 0; i < mTransports.size(); i++) {
+                AssociationInfo association = mAssociationStore.getAssociationById(
+                        mTransports.keyAt(i));
+                if (association != null) {
+                    associations.add(association);
+                }
             }
         }
         mTransportsListeners.broadcast(listener1 -> {
@@ -148,18 +150,19 @@
     /**
      * Send a message to remote devices through the transports
      */
-    @GuardedBy("mTransports")
     public void sendMessage(int message, byte[] data, int[] associationIds) {
         Slog.i(TAG, "Sending message 0x" + Integer.toHexString(message)
                 + " data length " + data.length);
-        for (int i = 0; i < associationIds.length; i++) {
-            if (mTransports.contains(associationIds[i])) {
-                try {
-                    mTransports.get(associationIds[i]).sendMessage(message, data);
-                } catch (IOException e) {
-                    Slog.e(TAG, "Failed to send message 0x" + Integer.toHexString(message)
-                            + " data length " + data.length + " to association "
-                            + associationIds[i]);
+        synchronized (mTransports) {
+            for (int i = 0; i < associationIds.length; i++) {
+                if (mTransports.contains(associationIds[i])) {
+                    try {
+                        mTransports.get(associationIds[i]).sendMessage(message, data);
+                    } catch (IOException e) {
+                        Slog.e(TAG, "Failed to send message 0x" + Integer.toHexString(message)
+                                + " data length " + data.length + " to association "
+                                + associationIds[i]);
+                    }
                 }
             }
         }
@@ -215,14 +218,15 @@
         }
     }
 
-    @GuardedBy("mTransports")
     private void notifyOnTransportsChanged() {
         List<AssociationInfo> associations = new ArrayList<>();
-        for (int i = 0; i < mTransports.size(); i++) {
-            AssociationInfo association = mAssociationStore.getAssociationById(
-                    mTransports.keyAt(i));
-            if (association != null) {
-                associations.add(association);
+        synchronized (mTransports) {
+            for (int i = 0; i < mTransports.size(); i++) {
+                AssociationInfo association = mAssociationStore.getAssociationById(
+                        mTransports.keyAt(i));
+                if (association != null) {
+                    associations.add(association);
+                }
             }
         }
         mTransportsListeners.broadcast(listener -> {
@@ -233,14 +237,15 @@
         });
     }
 
-    @GuardedBy("mTransports")
     private void initializeTransport(int associationId, ParcelFileDescriptor fd) {
         Slog.i(TAG, "Initializing transport");
         if (!isSecureTransportEnabled()) {
             Transport transport = new RawTransport(associationId, fd, mContext);
             addMessageListenersToTransport(transport);
             transport.start();
-            mTransports.put(associationId, transport);
+            synchronized (mTransports) {
+                mTransports.put(associationId, transport);
+            }
             Slog.i(TAG, "RawTransport is created");
             return;
         }
@@ -283,7 +288,6 @@
     /**
      * Depending on the remote platform info to decide which transport should be created
      */
-    @GuardedBy("CompanionTransportManager.this.mTransports")
     private void onPlatformInfoReceived(int associationId, byte[] data) {
         if (mTempTransport.getAssociationId() != associationId) {
             return;
@@ -330,7 +334,9 @@
         }
         addMessageListenersToTransport(transport);
         transport.start();
-        mTransports.put(transport.getAssociationId(), transport);
+        synchronized (mTransports) {
+            mTransports.put(transport.getAssociationId(), transport);
+        }
         // Doesn't need to notifyTransportsChanged here, it'll be done in attachSystemDataTransport
     }
 
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/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1f80aec..def2a2f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16707,6 +16707,11 @@
             pw.println(String.format("Resources History for %s (%s)",
                     app.processName,
                     app.info.packageName));
+            if (app.mOptRecord.isFrozen()) {
+                pw.println("  Skipping frozen process");
+                pw.flush();
+                continue;
+            }
             pw.flush();
             try {
                 TransferPipe tp = new TransferPipe("  ");
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/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index d926c2c..a181402 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -141,6 +141,7 @@
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
 
 /**
  * Helper class for {@link ActivityManagerService} responsible for multi-user functionality.
@@ -157,7 +158,7 @@
     private static final String TAG = TAG_WITH_CLASS_NAME ? "UserController" : TAG_AM;
 
     // Amount of time we wait for observers to handle a user switch before
-    // giving up on them and unfreezing the screen.
+    // giving up on them and dismissing the user switching dialog.
     static final int DEFAULT_USER_SWITCH_TIMEOUT_MS = 3 * 1000;
 
     /**
@@ -207,7 +208,7 @@
     /**
      * Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
      * called after dismissing the keyguard.
-     * Otherwise, we should move on to unfreeze the screen {@link #unfreezeScreen}
+     * Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
      * and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
      */
     private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
@@ -1695,14 +1696,6 @@
                 return false;
             }
 
-            if (foreground && isUserSwitchUiEnabled()) {
-                t.traceBegin("startFreezingScreen");
-                mInjector.getWindowManager().startFreezingScreen(
-                        R.anim.screen_user_exit, R.anim.screen_user_enter);
-                t.traceEnd();
-            }
-            dismissUserSwitchDialog(); // so that we don't hold a reference to mUserSwitchingDialog
-
             boolean needStart = false;
             boolean updateUmState = false;
             UserState uss;
@@ -1877,7 +1870,7 @@
         if (!success) {
             mInjector.getWindowManager().setSwitchingUser(false);
             mTargetUserId = UserHandle.USER_NULL;
-            dismissUserSwitchDialog();
+            dismissUserSwitchDialog(null);
         }
     }
 
@@ -2015,22 +2008,26 @@
             mUiHandler.sendMessage(mUiHandler.obtainMessage(
                     START_USER_SWITCH_UI_MSG, userNames));
         } else {
-            mHandler.removeMessages(START_USER_SWITCH_FG_MSG);
-            mHandler.sendMessage(mHandler.obtainMessage(
-                    START_USER_SWITCH_FG_MSG, targetUserId, 0));
+            sendStartUserSwitchFgMessage(targetUserId);
         }
         return true;
     }
 
-    private void dismissUserSwitchDialog() {
-        mInjector.dismissUserSwitchingDialog();
+    private void sendStartUserSwitchFgMessage(int targetUserId) {
+        mHandler.removeMessages(START_USER_SWITCH_FG_MSG);
+        mHandler.sendMessage(mHandler.obtainMessage(START_USER_SWITCH_FG_MSG, targetUserId, 0));
+    }
+
+    private void dismissUserSwitchDialog(Runnable onDismissed) {
+        mInjector.dismissUserSwitchingDialog(onDismissed);
     }
 
     private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
         // The dialog will show and then initiate the user switch by calling startUserInForeground
         mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
                 getSwitchingFromSystemUserMessageUnchecked(),
-                getSwitchingToSystemUserMessageUnchecked());
+                getSwitchingToSystemUserMessageUnchecked(),
+                /* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id));
     }
 
     private void dispatchForegroundProfileChanged(@UserIdInt int userId) {
@@ -2236,7 +2233,7 @@
 
         EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId);
 
-        // Do the keyguard dismiss and unfreeze later
+        // Do the keyguard dismiss and dismiss the user switching dialog later
         mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG);
         mHandler.sendMessage(mHandler.obtainMessage(
                 COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId));
@@ -2251,35 +2248,31 @@
     @VisibleForTesting
     void completeUserSwitch(int oldUserId, int newUserId) {
         final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
-        final Runnable runnable = () -> {
-            if (isUserSwitchUiEnabled) {
-                unfreezeScreen();
-            }
-            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
-            mHandler.sendMessage(mHandler.obtainMessage(
-                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
-        };
-
-        // If there is no challenge set, dismiss the keyguard right away
-        if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
-            // Wait until the keyguard is dismissed to unfreeze
-            mInjector.dismissKeyguard(runnable);
-        } else {
-            runnable.run();
-        }
+        // serialize each conditional step
+        await(
+                // STEP 1 - If there is no challenge set, dismiss the keyguard right away
+                isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
+                mInjector::dismissKeyguard,
+                () -> await(
+                        // STEP 2 - If user switch ui was enabled, dismiss user switch dialog
+                        isUserSwitchUiEnabled,
+                        this::dismissUserSwitchDialog,
+                        () -> {
+                            // STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
+                            // ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
+                            mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
+                            mHandler.sendMessage(mHandler.obtainMessage(
+                                    REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
+                        }
+                ));
     }
 
-    /**
-     * Tell WindowManager we're ready to unfreeze the screen, at its leisure. Note that there is
-     * likely a lot going on, and WM won't unfreeze until the drawing is all done, so
-     * the actual unfreeze may still not happen for a long time; this is expected.
-     */
-    @VisibleForTesting
-    void unfreezeScreen() {
-        TimingsTraceAndSlog t = new TimingsTraceAndSlog();
-        t.traceBegin("stopFreezingScreen");
-        mInjector.getWindowManager().stopFreezingScreen();
-        t.traceEnd();
+    private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
+        if (condition) {
+            conditionalStep.accept(nextStep);
+        } else {
+            nextStep.run();
+        }
     }
 
     private void moveUserToForeground(UserState uss, int newUserId) {
@@ -3731,17 +3724,18 @@
             mService.mCpHelper.installEncryptionUnawareProviders(userId);
         }
 
-        void dismissUserSwitchingDialog() {
+        void dismissUserSwitchingDialog(@Nullable Runnable onDismissed) {
             synchronized (mUserSwitchingDialogLock) {
                 if (mUserSwitchingDialog != null) {
-                    mUserSwitchingDialog.dismiss();
+                    mUserSwitchingDialog.dismiss(onDismissed);
                     mUserSwitchingDialog = null;
                 }
             }
         }
 
         void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
-                String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
+                String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
+                @NonNull Runnable onShown) {
             if (mService.mContext.getPackageManager()
                     .hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
                 // config_customUserSwitchUi is set to true on Automotive as CarSystemUI is
@@ -3751,11 +3745,10 @@
                         + "condition if it's shown by CarSystemUI as well");
             }
             synchronized (mUserSwitchingDialogLock) {
-                dismissUserSwitchingDialog();
-                mUserSwitchingDialog = new UserSwitchingDialog(mService, mService.mContext,
-                        fromUser, toUser, true /* above system */, switchingFromSystemUserMessage,
-                        switchingToSystemUserMessage);
-                mUserSwitchingDialog.show();
+                dismissUserSwitchingDialog(null);
+                mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser,
+                        switchingFromSystemUserMessage, switchingToSystemUserMessage);
+                mUserSwitchingDialog.show(onShown);
             }
         }
 
diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java
index a5651bf..649305f 100644
--- a/services/core/java/com/android/server/am/UserSwitchingDialog.java
+++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java
@@ -16,160 +16,258 @@
 
 package com.android.server.am;
 
-import android.app.AlertDialog;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.Dialog;
 import android.content.Context;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Message;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.SystemProperties;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Slog;
-import android.view.LayoutInflater;
+import android.util.TypedValue;
 import android.view.View;
-import android.view.ViewTreeObserver;
+import android.view.Window;
 import android.view.WindowManager;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import com.android.internal.R;
-import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.ObjectUtils;
+import com.android.internal.util.UserIcons;
 
 /**
- * Dialog to show when a user switch it about to happen. The intent is to snapshot the screen
- * immediately after the dialog shows so that the user is informed that something is happening
- * in the background rather than just freeze the screen and not know if the user-switch affordance
- * was being handled.
+ * Dialog to show during the user switch. This dialog shows target user's name and their profile
+ * picture with a circular spinner animation around it if the animations for this dialog are not
+ * disabled. And covers the whole screen so that all the UI jank caused by the switch are hidden.
  */
-class UserSwitchingDialog extends AlertDialog
-        implements ViewTreeObserver.OnWindowShownListener {
-    private static final String TAG = "ActivityManagerUserSwitchingDialog";
-
-    // Time to wait for the onWindowShown() callback before continuing the user switch
-    private static final int WINDOW_SHOWN_TIMEOUT_MS = 3000;
+class UserSwitchingDialog extends Dialog {
+    private static final String TAG = "UserSwitchingDialog";
+    private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
 
     // User switching doesn't happen that frequently, so it doesn't hurt to have it always on
     protected static final boolean DEBUG = true;
+    private static final long DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS = 300;
+    private final boolean mDisableAnimations;
 
-    private final ActivityManagerService mService;
-    private final int mUserId;
-    private static final int MSG_START_USER = 1;
-    @GuardedBy("this")
-    private boolean mStartedUser;
-    final protected UserInfo mOldUser;
-    final protected UserInfo mNewUser;
-    final private String mSwitchingFromSystemUserMessage;
-    final private String mSwitchingToSystemUserMessage;
-    final protected Context mContext;
+    protected final UserInfo mOldUser;
+    protected final UserInfo mNewUser;
+    private final String mSwitchingFromSystemUserMessage;
+    private final String mSwitchingToSystemUserMessage;
+    protected final Context mContext;
+    private final int mTraceCookie;
 
-    public UserSwitchingDialog(ActivityManagerService service, Context context, UserInfo oldUser,
-            UserInfo newUser, boolean aboveSystem, String switchingFromSystemUserMessage,
-            String switchingToSystemUserMessage) {
-        super(context);
+    UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser,
+            String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
+        // TODO(b/278857848): Make full screen user switcher cover top part of the screen as well.
+        //                    This problem is seen only on phones, it works fine on tablets.
+        super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
 
         mContext = context;
-        mService = service;
-        mUserId = newUser.id;
         mOldUser = oldUser;
         mNewUser = newUser;
         mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
         mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+        mDisableAnimations = ActivityManager.isLowRamDeviceStatic() || SystemProperties.getBoolean(
+                "debug.usercontroller.disable_user_switching_dialog_animations", false);
+        mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
 
         inflateContent();
+        configureWindow();
+    }
 
-        if (aboveSystem) {
-            getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
-        }
-
-        WindowManager.LayoutParams attrs = getWindow().getAttributes();
+    private void configureWindow() {
+        final Window window = getWindow();
+        final WindowManager.LayoutParams attrs = window.getAttributes();
         attrs.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR |
-            WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
-        getWindow().setAttributes(attrs);
+                WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+        window.setAttributes(attrs);
+        window.setBackgroundDrawableResource(android.R.color.transparent);
+        window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
     }
 
     void inflateContent() {
-        // Set up the dialog contents
         setCancelable(false);
-        Resources res = getContext().getResources();
-        // Custom view due to alignment and font size requirements
-        TextView view = (TextView) LayoutInflater.from(getContext()).inflate(
-                R.layout.user_switching_dialog, null);
+        setContentView(R.layout.user_switching_dialog);
 
-        String viewMessage = null;
-        if (UserManager.isDeviceInDemoMode(mContext)) {
-            if (mOldUser.isDemo()) {
-                viewMessage = res.getString(R.string.demo_restarting_message);
-            } else {
-                viewMessage = res.getString(R.string.demo_starting_message);
-            }
-        } else {
-            if (mOldUser.id == UserHandle.USER_SYSTEM) {
-                viewMessage = mSwitchingFromSystemUserMessage;
-            } else if (mNewUser.id == UserHandle.USER_SYSTEM) {
-                viewMessage = mSwitchingToSystemUserMessage;
-            }
-
-            // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null, fallback
-            // to system message.
-            if (viewMessage == null) {
-                viewMessage = res.getString(R.string.user_switching_message, mNewUser.name);
-            }
-
-            view.setCompoundDrawablesWithIntrinsicBounds(null,
-                    getContext().getDrawable(R.drawable.ic_swap_horiz), null, null);
+        final TextView textView = findViewById(R.id.message);
+        if (textView != null) {
+            final String message = getTextMessage();
+            textView.setAccessibilityPaneTitle(message);
+            textView.setText(message);
         }
-        view.setAccessibilityPaneTitle(viewMessage);
-        view.setText(viewMessage);
-        setView(view);
+
+        final ImageView imageView = findViewById(R.id.icon);
+        if (imageView != null) {
+            imageView.setImageBitmap(getUserIconRounded());
+        }
+
+        final ImageView progressCircular = findViewById(R.id.progress_circular);
+        if (progressCircular != null) {
+            if (mDisableAnimations) {
+                progressCircular.setVisibility(View.GONE);
+            } else {
+                final TypedValue value = new TypedValue();
+                getContext().getTheme().resolveAttribute(R.attr.colorAccentPrimary, value, true);
+                progressCircular.setColorFilter(value.data);
+            }
+        }
+    }
+
+    private Bitmap getUserIconRounded() {
+        final Bitmap bmp = ObjectUtils.getOrElse(BitmapFactory.decodeFile(mNewUser.iconPath),
+                defaultUserIcon(mNewUser.id));
+        final int w = bmp.getWidth();
+        final int h = bmp.getHeight();
+        final Bitmap bmpRounded = Bitmap.createBitmap(w, h, bmp.getConfig());
+        final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setShader(new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
+        new Canvas(bmpRounded).drawRoundRect((new RectF(0, 0, w, h)), w / 2f, h / 2f, paint);
+        return bmpRounded;
+    }
+
+    private Bitmap defaultUserIcon(@UserIdInt int userId) {
+        final Resources res = getContext().getResources();
+        final Drawable icon = UserIcons.getDefaultUserIcon(res, userId, /* light= */ false);
+        return UserIcons.convertToBitmapAtUserIconSize(res, icon);
+    }
+
+    private String getTextMessage() {
+        final Resources res = getContext().getResources();
+
+        if (UserManager.isDeviceInDemoMode(mContext)) {
+            return res.getString(mOldUser.isDemo()
+                    ? R.string.demo_restarting_message
+                    : R.string.demo_starting_message);
+        }
+
+        final String message =
+                mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage
+                : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null;
+
+        return message != null ? message
+                // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null,
+                // fallback to system message.
+                : res.getString(R.string.user_switching_message, mNewUser.name);
     }
 
     @Override
     public void show() {
-        if (DEBUG) Slog.d(TAG, "show called");
+        asyncTraceBegin("", 0);
         super.show();
-        final View decorView = getWindow().getDecorView();
-        if (decorView != null) {
-            decorView.getViewTreeObserver().addOnWindowShownListener(this);
-        }
-        // Add a timeout as a safeguard, in case a race in screen on/off causes the window
-        // callback to never come.
-        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_USER),
-                WINDOW_SHOWN_TIMEOUT_MS);
     }
 
     @Override
-    public void onWindowShown() {
-        if (DEBUG) Slog.d(TAG, "onWindowShown called");
-        startUser();
+    public void dismiss() {
+        super.dismiss();
+        asyncTraceEnd("", 0);
     }
 
-    void startUser() {
-        synchronized (this) {
-            if (!mStartedUser) {
-                Slog.i(TAG, "starting user " + mUserId);
-                mService.mUserController.startUserInForeground(mUserId);
+    public void show(@NonNull Runnable onShown) {
+        if (DEBUG) Slog.d(TAG, "show called");
+        show();
+
+        if (mDisableAnimations) {
+            onShown.run();
+        } else {
+            startShowAnimation(onShown);
+        }
+    }
+
+    public void dismiss(@Nullable Runnable onDismissed) {
+        if (DEBUG) Slog.d(TAG, "dismiss called");
+
+        if (onDismissed == null) {
+            // no animation needed
+            dismiss();
+        } else if (mDisableAnimations) {
+            dismiss();
+            onDismissed.run();
+        } else {
+            startDismissAnimation(() -> {
                 dismiss();
-                mStartedUser = true;
-                final View decorView = getWindow().getDecorView();
-                if (decorView != null) {
-                    decorView.getViewTreeObserver().removeOnWindowShownListener(this);
-                }
-                mHandler.removeMessages(MSG_START_USER);
-            } else {
-                Slog.i(TAG, "user " + mUserId + " already started");
-            }
+                onDismissed.run();
+            });
         }
     }
 
-    private final Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_START_USER:
-                    Slog.w(TAG, "user switch window not shown in "
-                            + WINDOW_SHOWN_TIMEOUT_MS + " ms");
-                    startUser();
-                    break;
+    private void startShowAnimation(Runnable onAnimationEnd) {
+        asyncTraceBegin("-showAnimation", 1);
+        startDialogAnimation(new AlphaAnimation(0, 1), () -> {
+            asyncTraceEnd("-showAnimation", 1);
+
+            asyncTraceBegin("-spinnerAnimation", 2);
+            startProgressAnimation(() -> {
+                asyncTraceEnd("-spinnerAnimation", 2);
+
+                onAnimationEnd.run();
+            });
+        });
+    }
+
+    private void startDismissAnimation(Runnable onAnimationEnd) {
+        asyncTraceBegin("-dismissAnimation", 3);
+        startDialogAnimation(new AlphaAnimation(1, 0), () -> {
+            asyncTraceEnd("-dismissAnimation", 3);
+
+            onAnimationEnd.run();
+        });
+    }
+
+    private void startProgressAnimation(Runnable onAnimationEnd) {
+        final ImageView progressCircular = findViewById(R.id.progress_circular);
+        final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) progressCircular.getDrawable();
+        avd.registerAnimationCallback(new Animatable2.AnimationCallback() {
+            @Override
+            public void onAnimationEnd(Drawable drawable) {
+                onAnimationEnd.run();
             }
-        }
-    };
+        });
+        avd.start();
+    }
+
+    private void startDialogAnimation(Animation animation, Runnable onAnimationEnd) {
+        animation.setDuration(DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS);
+        animation.setAnimationListener(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) {
+
+            }
+
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                onAnimationEnd.run();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) {
+
+            }
+        });
+        findViewById(R.id.content).startAnimation(animation);
+    }
+
+    private void asyncTraceBegin(String subTag, int subCookie) {
+        Trace.asyncTraceBegin(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
+    }
+
+    private void asyncTraceEnd(String subTag, int subCookie) {
+        Trace.asyncTraceEnd(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
+    }
 }
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/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/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 07891f3..d1d6f5f 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -693,6 +693,7 @@
     private NotificationRecordLogger mNotificationRecordLogger;
     private InstanceIdSequence mNotificationInstanceIdSequence;
     private Set<String> mMsgPkgsAllowedAsConvos = new HashSet();
+    private String mDefaultSearchSelectorPkg;
 
     // Broadcast intent receiver for notification permissions review-related intents
     private ReviewNotificationPermissionsReceiver mReviewNotificationPermissionsReceiver;
@@ -2435,6 +2436,8 @@
 
         mMsgPkgsAllowedAsConvos = Set.of(getStringArrayResource(
                 com.android.internal.R.array.config_notificationMsgPkgsAllowedAsConvos));
+        mDefaultSearchSelectorPkg = getContext().getString(getContext().getResources()
+                .getIdentifier("config_defaultSearchSelectorPackageName", "string", "android"));
 
         mFlagResolver = flagResolver;
 
@@ -6934,7 +6937,12 @@
      */
     private boolean canBeNonDismissible(ApplicationInfo ai, Notification notification) {
         return notification.isMediaNotification() || isEnterpriseExempted(ai)
-                || isCallNotification(ai.packageName, ai.uid, notification);
+                || isCallNotification(ai.packageName, ai.uid, notification)
+                || isDefaultSearchSelectorPackage(ai.packageName);
+    }
+
+    private boolean isDefaultSearchSelectorPackage(String pkg) {
+        return Objects.equals(mDefaultSearchSelectorPkg, pkg);
     }
 
     private boolean isEnterpriseExempted(ApplicationInfo ai) {
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 7a78efb..aeadcd5 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;
@@ -4571,6 +4576,12 @@
 
             case KeyEvent.KEYCODE_BACK:
                 return mWakeOnBackKeyPress;
+
+            case KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY:
+            case KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY:
+            case KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY:
+            case KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL:
+                return mStylusButtonsEnabled;
         }
 
         return true;
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/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/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index f93afe8..ea0731a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1996,6 +1996,24 @@
         }
     }
 
+    @Override
+    public void focusTopTask(int displayId) {
+        enforceTaskPermission("focusTopTask()");
+        final long callingId = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId);
+                if (dc == null) return;
+                final Task task = dc.getTask((t) -> t.isLeafTask() && t.isFocusable(),
+                        true /*  traverseTopToBottom */);
+                if (task == null) return;
+                setFocusedTask(task.mTaskId, null /* touchedActivity */);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(callingId);
+        }
+    }
+
     void setFocusedTask(int taskId, ActivityRecord touchedActivity) {
         ProtoLog.d(WM_DEBUG_FOCUS, "setFocusedTask: taskId=%d touchedActivity=%s", taskId,
                 touchedActivity);
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..e8e4792 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,10 +180,20 @@
         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() {
         if (mTransitionPlayer == null) return;
+        // Immediately set to null so that nothing inadvertently starts/queues.
+        mTransitionPlayer = null;
         // Clean-up/finish any playing transitions.
         for (int i = 0; i < mPlayingTransitions.size(); ++i) {
             mPlayingTransitions.get(i).cleanUpOnFailure();
@@ -170,7 +202,6 @@
         if (mCollectingTransition != null) {
             mCollectingTransition.abort();
         }
-        mTransitionPlayer = null;
         mTransitionPlayerProc = null;
         mRemotePlayer.clear();
         mRunningLock.doNotifyLocked();
@@ -195,7 +226,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 +356,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 +484,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 +771,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 +804,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 +814,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 +849,7 @@
         transition.abort();
         mCollectingTransition = null;
         mTransitionTracer.logState(transition);
+        // abort will call through the normal finish paths and thus check the queue.
     }
 
     /**
@@ -874,7 +931,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 +942,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/OWNERS b/services/core/jni/OWNERS
index 2584b86..d9acf41 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -27,3 +27,4 @@
 per-file com_android_server_tv_* = file:/media/java/android/media/tv/OWNERS
 per-file com_android_server_vibrator_* = file:/services/core/java/com/android/server/vibrator/OWNERS
 per-file com_android_server_am_CachedAppOptimizer.cpp = timmurray@google.com, edgararriaga@google.com, dualli@google.com, carmenjackson@google.com, philipcuadra@google.com
+per-file com_android_server_companion_virtual_InputController.cpp = file:/services/companion/java/com/android/server/companion/virtual/OWNERS
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/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index f39de43..0271727 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -19,7 +19,6 @@
 import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
-import android.credentials.CredentialOption;
 import android.credentials.CredentialProviderInfo;
 import android.credentials.GetCredentialException;
 import android.credentials.GetCredentialRequest;
@@ -36,7 +35,6 @@
 
 import java.util.ArrayList;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * Central session for a single getCredentials request. This class listens to the
@@ -56,11 +54,7 @@
         super(context, sessionCallback, lock, userId, callingUid, request, callback,
                 RequestInfo.TYPE_GET, callingAppInfo, enabledProviders, cancellationSignal,
                 startedTimestamp);
-        int numTypes = (request.getCredentialOptions().stream()
-                .map(CredentialOption::getType).collect(
-                        Collectors.toSet())).size(); // Dedupe type strings
-        mRequestSessionMetric.collectGetFlowInitialMetricInfo(numTypes,
-                /*origin=*/request.getOrigin() != null);
+        mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
     }
 
     /**
diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
index 47b45ac..50e5163 100644
--- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java
+++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java
@@ -50,6 +50,8 @@
     public static final int UNIT = 1;
     // Used for zero count metric emits, such as zero amounts of various types
     public static final int ZERO = 0;
+    // The number of characters at the end of the string to use as a key
+    public static final int DELTA_CUT = 20;
 
     /**
      * This retrieves the uid of any package name, given a context and a component name for the
@@ -87,6 +89,18 @@
     }
 
     /**
+     * Given the current design, we can designate how the strings in the backend should appear.
+     * This helper method lets us cut strings for our class types.
+     *
+     * @param classtype the classtype string we want to cut to generate a key
+     * @param deltaFromEnd the starting point from the end of the string we wish to begin at
+     * @return the cut up string key we want to use for metric logs
+     */
+    public static String generateMetricKey(String classtype, int deltaFromEnd) {
+        return classtype.substring(classtype.length() - deltaFromEnd);
+    }
+
+    /**
      * A logging utility used primarily for the final phase of the current metric setup.
      *
      * @param finalPhaseMetric     the coalesced data of the chosen provider
@@ -158,13 +172,17 @@
     }
 
     /**
-     * A logging utility used primarily for the candidate phase of the current metric setup.
+     * A logging utility used primarily for the candidate phase of the current metric setup. This
+     * will primarily focus on track 2, where the session id is associated with known providers,
+     * but NOT the calling app.
      *
      * @param providers      a map with known providers and their held metric objects
      * @param emitSequenceId an emitted sequence id for the current session
+     * @param initialPhaseMetric contains initial phase data to avoid repetition for candidate
+     *                           phase, track 2, logging
      */
     public static void logApiCalledCandidatePhase(Map<String, ProviderSession> providers,
-            int emitSequenceId) {
+            int emitSequenceId, InitialPhaseMetric initialPhaseMetric) {
         try {
             if (!LOG_FLAG) {
                 return;
@@ -184,6 +202,7 @@
             int[] candidateActionEntryCountList = new int[providerSize];
             int[] candidateAuthEntryCountList = new int[providerSize];
             int[] candidateRemoteEntryCountList = new int[providerSize];
+            String[] frameworkExceptionList = new String[providerSize];
             int index = 0;
             for (var session : providerSessions) {
                 CandidatePhaseMetric metric = session.mProviderSessionMetric
@@ -209,6 +228,7 @@
                 candidateActionEntryCountList[index] = metric.getActionEntryCount();
                 candidateAuthEntryCountList[index] = metric.getAuthenticationEntryCount();
                 candidateRemoteEntryCountList[index] = metric.getRemoteEntryCount();
+                frameworkExceptionList[index] = metric.getFrameworkException();
                 index++;
             }
             FrameworkStatsLog.write(FrameworkStatsLog.CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED,
@@ -229,11 +249,16 @@
                     /* candidate_provider_credential_entry_type_count */
                     candidateCredentialTypeCountList,
                     /* candidate_provider_remote_entry_count */ candidateRemoteEntryCountList,
-                    /* candidate_provider_authentication_entry_count */ candidateAuthEntryCountList,
-                    DEFAULT_REPEATED_STR,
-                    false,
-                    DEFAULT_REPEATED_STR,
-                    DEFAULT_REPEATED_INT_32
+                    /* candidate_provider_authentication_entry_count */
+                    candidateAuthEntryCountList,
+                    /* framework_exception_per_provider */
+                    frameworkExceptionList,
+                    /* origin_specified originSpecified */
+                    initialPhaseMetric.isOriginSpecified(),
+                    /* request_unique_classtypes */
+                    initialPhaseMetric.getUniqueRequestStrings(),
+                    /* per_classtype_counts */
+                    initialPhaseMetric.getUniqueRequestCounts()
             );
         } catch (Exception e) {
             Log.w(TAG, "Unexpected error during metric logging: " + e);
@@ -297,10 +322,11 @@
                     initialPhaseMetric.getCredentialServiceStartedTimeNanoseconds(),
                     /* count_credential_request_classtypes */
                     initialPhaseMetric.getCountRequestClassType(),
-                    // TODO(b/271135048) - add total count of request options
-                    // TODO(b/271135048) - Uncomment once built past PWG review -
-                    DEFAULT_REPEATED_STR,
-                    DEFAULT_REPEATED_INT_32,
+                    /* request_unique_classtypes */
+                    initialPhaseMetric.getUniqueRequestStrings(),
+                    /* per_classtype_counts */
+                    initialPhaseMetric.getUniqueRequestCounts(),
+                    /* origin_specified */
                     initialPhaseMetric.isOriginSpecified()
             );
         } catch (Exception e) {
diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
index 1c3d213c..441c87b 100644
--- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java
@@ -59,8 +59,7 @@
         int numTypes = (request.getCredentialOptions().stream()
                 .map(CredentialOption::getType).collect(
                         Collectors.toSet())).size(); // Dedupe type strings
-        mRequestSessionMetric.collectGetFlowInitialMetricInfo(numTypes,
-                /*origin=*/request.getOrigin() != null);
+        mRequestSessionMetric.collectGetFlowInitialMetricInfo(request);
         mPrepareGetCredentialCallback = prepareGetCredentialCallback;
     }
 
diff --git a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
index 9ec0ecd..8af6b56 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderClearSession.java
@@ -91,6 +91,8 @@
     public void onProviderResponseFailure(int errorCode, Exception exception) {
         if (exception instanceof ClearCredentialStateException) {
             mProviderException = (ClearCredentialStateException) exception;
+            // TODO(b/271135048) : Decide on exception type length
+            mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType());
         }
         mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true);
         updateStatusAndInvokeCallback(toStatus(errorCode),
diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
index 09433db..520b937 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java
@@ -155,6 +155,8 @@
         if (exception instanceof CreateCredentialException) {
             // Store query phase exception for aggregation with final response
             mProviderException = (CreateCredentialException) exception;
+            // TODO(b/271135048) : Decide on exception type length
+            mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType());
         }
         mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true);
         updateStatusAndInvokeCallback(toStatus(errorCode),
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 0c2b563..a62d9e8 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -217,6 +217,8 @@
     public void onProviderResponseFailure(int errorCode, Exception exception) {
         if (exception instanceof GetCredentialException) {
             mProviderException = (GetCredentialException) exception;
+            // TODO(b/271135048) : Decide on exception type length
+            mProviderSessionMetric.collectCandidateFrameworkException(mProviderException.getType());
         }
         mProviderSessionMetric.collectCandidateExceptionStatus(/*hasException=*/true);
         updateStatusAndInvokeCallback(toStatus(errorCode),
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java
index ce84d9a..b99f28d 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ApiName.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ApiName.java
@@ -57,7 +57,7 @@
     );
 
     ApiName(int innerMetricCode) {
-        this.mInnerMetricCode = innerMetricCode;
+        mInnerMetricCode = innerMetricCode;
     }
 
     /**
@@ -66,7 +66,7 @@
      * @return a code corresponding to the west world metric name
      */
     public int getMetricCode() {
-        return this.mInnerMetricCode;
+        return mInnerMetricCode;
     }
 
     /**
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
index 4097765..ece729f 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ApiStatus.java
@@ -32,7 +32,7 @@
     private final int mInnerMetricCode;
 
     ApiStatus(int innerMetricCode) {
-        this.mInnerMetricCode = innerMetricCode;
+        mInnerMetricCode = innerMetricCode;
     }
 
     /**
@@ -41,6 +41,6 @@
      * @return a code corresponding to the west world metric name
      */
     public int getMetricCode() {
-        return this.mInnerMetricCode;
+        return mInnerMetricCode;
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
index 10d4f9c..721d3d7 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/CandidatePhaseMetric.java
@@ -73,6 +73,8 @@
     private int mAuthenticationEntryCount = -1;
     // Gathered to pass on to chosen provider when required
     private final IntArray mAvailableEntries = new IntArray();
+    // The *framework only* exception held by this provider, empty string by default
+    private String mFrameworkException = "";
 
     public CandidatePhaseMetric() {
     }
@@ -82,27 +84,27 @@
     /* -- Timestamps -- */
 
     public void setServiceBeganTimeNanoseconds(long serviceBeganTimeNanoseconds) {
-        this.mServiceBeganTimeNanoseconds = serviceBeganTimeNanoseconds;
+        mServiceBeganTimeNanoseconds = serviceBeganTimeNanoseconds;
     }
 
     public void setStartQueryTimeNanoseconds(long startQueryTimeNanoseconds) {
-        this.mStartQueryTimeNanoseconds = startQueryTimeNanoseconds;
+        mStartQueryTimeNanoseconds = startQueryTimeNanoseconds;
     }
 
     public void setQueryFinishTimeNanoseconds(long queryFinishTimeNanoseconds) {
-        this.mQueryFinishTimeNanoseconds = queryFinishTimeNanoseconds;
+        mQueryFinishTimeNanoseconds = queryFinishTimeNanoseconds;
     }
 
     public long getServiceBeganTimeNanoseconds() {
-        return this.mServiceBeganTimeNanoseconds;
+        return mServiceBeganTimeNanoseconds;
     }
 
     public long getStartQueryTimeNanoseconds() {
-        return this.mStartQueryTimeNanoseconds;
+        return mStartQueryTimeNanoseconds;
     }
 
     public long getQueryFinishTimeNanoseconds() {
-        return this.mQueryFinishTimeNanoseconds;
+        return mQueryFinishTimeNanoseconds;
     }
 
     /* -- Actual time delta latencies (for local utility) -- */
@@ -111,8 +113,8 @@
      * Returns the latency in microseconds for the query phase.
      */
     public int getQueryLatencyMicroseconds() {
-        return (int) ((this.getQueryFinishTimeNanoseconds()
-                - this.getStartQueryTimeNanoseconds()) / 1000);
+        return (int) ((getQueryFinishTimeNanoseconds()
+                - getStartQueryTimeNanoseconds()) / 1000);
     }
 
     /* --- Time Stamp Conversion to Microseconds from Reference --- */
@@ -126,32 +128,32 @@
      * @return the microsecond integer timestamp from service start to query began
      */
     public int getTimestampFromReferenceStartMicroseconds(long specificTimestamp) {
-        if (specificTimestamp < this.mServiceBeganTimeNanoseconds) {
+        if (specificTimestamp < mServiceBeganTimeNanoseconds) {
             Log.i(TAG, "The timestamp is before service started, falling back to default int");
             return MetricUtilities.DEFAULT_INT_32;
         }
         return (int) ((specificTimestamp
-                - this.mServiceBeganTimeNanoseconds) / 1000);
+                - mServiceBeganTimeNanoseconds) / 1000);
     }
 
     /* ------------- Provider Query Status ------------ */
 
     public void setProviderQueryStatus(int providerQueryStatus) {
-        this.mProviderQueryStatus = providerQueryStatus;
+        mProviderQueryStatus = providerQueryStatus;
     }
 
     public int getProviderQueryStatus() {
-        return this.mProviderQueryStatus;
+        return mProviderQueryStatus;
     }
 
     /* -------------- Candidate Uid ---------------- */
 
     public void setCandidateUid(int candidateUid) {
-        this.mCandidateUid = candidateUid;
+        mCandidateUid = candidateUid;
     }
 
     public int getCandidateUid() {
-        return this.mCandidateUid;
+        return mCandidateUid;
     }
 
     /* -------------- Session Id ---------------- */
@@ -254,7 +256,7 @@
      *          collector
      */
     public void addEntry(EntryEnum e) {
-        this.mAvailableEntries.add(e.getMetricCode());
+        mAvailableEntries.add(e.getMetricCode());
     }
 
     /**
@@ -267,4 +269,14 @@
     public List<Integer> getAvailableEntries() {
         return Arrays.stream(mAvailableEntries.toArray()).boxed().collect(Collectors.toList());
     }
+
+    /* ------ Framework Exception for this Candidate ------ */
+
+    public void setFrameworkException(String frameworkException) {
+        mFrameworkException = frameworkException;
+    }
+
+    public String getFrameworkException() {
+        return mFrameworkException;
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
index 2eef197..c80cc24 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ChosenProviderFinalPhaseMetric.java
@@ -138,8 +138,8 @@
     }
 
     public int getUiPhaseLatencyMicroseconds() {
-        return (int) ((this.mUiCallEndTimeNanoseconds
-                - this.mUiCallStartTimeNanoseconds) / 1000);
+        return (int) ((mUiCallEndTimeNanoseconds
+                - mUiCallStartTimeNanoseconds) / 1000);
     }
 
     /**
@@ -147,8 +147,8 @@
      * start time to be provided, such as from {@link CandidatePhaseMetric}.
      */
     public int getEntireProviderLatencyMicroseconds() {
-        return (int) ((this.mFinalFinishTimeNanoseconds
-                - this.mQueryStartTimeNanoseconds) / 1000);
+        return (int) ((mFinalFinishTimeNanoseconds
+                - mQueryStartTimeNanoseconds) / 1000);
     }
 
     /**
@@ -156,8 +156,8 @@
      * start time to be provided, such as from {@link InitialPhaseMetric}.
      */
     public int getEntireLatencyMicroseconds() {
-        return (int) ((this.mFinalFinishTimeNanoseconds
-                - this.mServiceBeganTimeNanoseconds) / 1000);
+        return (int) ((mFinalFinishTimeNanoseconds
+                - mServiceBeganTimeNanoseconds) / 1000);
     }
 
     /* ----- Timestamps for Latency ----- */
@@ -183,11 +183,11 @@
     }
 
     public void setUiCallStartTimeNanoseconds(long uiCallStartTimeNanoseconds) {
-        this.mUiCallStartTimeNanoseconds = uiCallStartTimeNanoseconds;
+        mUiCallStartTimeNanoseconds = uiCallStartTimeNanoseconds;
     }
 
     public void setUiCallEndTimeNanoseconds(long uiCallEndTimeNanoseconds) {
-        this.mUiCallEndTimeNanoseconds = uiCallEndTimeNanoseconds;
+        mUiCallEndTimeNanoseconds = uiCallEndTimeNanoseconds;
     }
 
     public void setFinalFinishTimeNanoseconds(long finalFinishTimeNanoseconds) {
@@ -229,12 +229,12 @@
      * @return the microsecond integer timestamp from service start to query began
      */
     public int getTimestampFromReferenceStartMicroseconds(long specificTimestamp) {
-        if (specificTimestamp < this.mServiceBeganTimeNanoseconds) {
+        if (specificTimestamp < mServiceBeganTimeNanoseconds) {
             Log.i(TAG, "The timestamp is before service started, falling back to default int");
             return MetricUtilities.DEFAULT_INT_32;
         }
         return (int) ((specificTimestamp
-                - this.mServiceBeganTimeNanoseconds) / 1000);
+                - mServiceBeganTimeNanoseconds) / 1000);
     }
 
     /* ----------- Provider Status -------------- */
@@ -334,7 +334,7 @@
      * chosen phase in a semantically correct way.
      */
     public void setAvailableEntries(List<Integer> entries) {
-        this.mAvailableEntries = new ArrayList<>(entries); // no alias copy
+        mAvailableEntries = new ArrayList<>(entries); // no alias copy
     }
 
     /**
@@ -345,7 +345,7 @@
      * candidate phase.
      */
     public List<Integer> getAvailableEntries() {
-        return new ArrayList<>(this.mAvailableEntries); // no alias copy
+        return new ArrayList<>(mAvailableEntries); // no alias copy
     }
 
     /* -------------- Has Exception ---------------- */
diff --git a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
index 80f9fdc..b9125dd 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/EntryEnum.java
@@ -56,7 +56,7 @@
     );
 
     EntryEnum(int innerMetricCode) {
-        this.mInnerMetricCode = innerMetricCode;
+        mInnerMetricCode = innerMetricCode;
     }
 
     /**
@@ -65,7 +65,7 @@
      * @return a code corresponding to the west world metric name
      */
     public int getMetricCode() {
-        return this.mInnerMetricCode;
+        return mInnerMetricCode;
     }
 
     /**
diff --git a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
index 0210b14..0ecd9cc 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/InitialPhaseMetric.java
@@ -16,6 +16,11 @@
 
 package com.android.server.credentials.metrics;
 
+import android.util.Log;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 /**
  * This handles metrics collected prior to any remote calls to providers.
  * Some types are redundant across these metric collectors, but that has debug use-cases as
@@ -32,7 +37,6 @@
     private int mCallerUid = -1;
     // The session id to unite multiple atom emits, default to -1
     private int mSessionId = -1;
-    private int mCountRequestClassType = -1;
 
     // Raw timestamps in nanoseconds, *the only* one logged as such (i.e. 64 bits) since it is a
     // reference point.
@@ -46,6 +50,9 @@
     // TODO(b/271135048) - Emit once metrics approved
     private boolean mOriginSpecified = false;
 
+    // Stores the deduped request information, particularly {"req":5}.
+    private Map<String, Integer> mRequestCounts = new LinkedHashMap<>();
+
 
     public InitialPhaseMetric() {
     }
@@ -55,8 +62,8 @@
     /* -- Direct Latency Utility -- */
 
     public int getServiceStartToQueryLatencyMicroseconds() {
-        return (int) ((this.mCredentialServiceStartedTimeNanoseconds
-                - this.mCredentialServiceBeginQueryTimeNanoseconds) / 1000);
+        return (int) ((mCredentialServiceStartedTimeNanoseconds
+                - mCredentialServiceBeginQueryTimeNanoseconds) / 1000);
     }
 
     /* -- Timestamps -- */
@@ -64,7 +71,7 @@
     public void setCredentialServiceStartedTimeNanoseconds(
             long credentialServiceStartedTimeNanoseconds
     ) {
-        this.mCredentialServiceStartedTimeNanoseconds = credentialServiceStartedTimeNanoseconds;
+        mCredentialServiceStartedTimeNanoseconds = credentialServiceStartedTimeNanoseconds;
     }
 
     public void setCredentialServiceBeginQueryTimeNanoseconds(
@@ -112,13 +119,11 @@
 
     /* ------ Count Request Class Types ------ */
 
-    public void setCountRequestClassType(int countRequestClassType) {
-        mCountRequestClassType = countRequestClassType;
+    public int getCountRequestClassType() {
+        return mRequestCounts.size();
     }
 
-    public int getCountRequestClassType() {
-        return mCountRequestClassType;
-    }
+    /* ------ Origin Specified ------ */
 
     public void setOriginSpecified(boolean originSpecified) {
         mOriginSpecified = originSpecified;
@@ -127,4 +132,34 @@
     public boolean isOriginSpecified() {
         return mOriginSpecified;
     }
+
+    /* ------ Unique Request Counts Map Information ------ */
+
+    public void setRequestCounts(Map<String, Integer> requestCounts) {
+        mRequestCounts = requestCounts;
+    }
+
+    /**
+     * Reruns the unique, deduped, request classtypes for logging.
+     * @return a string array for deduped classtypes
+     */
+    public String[] getUniqueRequestStrings() {
+        if (mRequestCounts.isEmpty()) {
+            Log.w(TAG, "There are no unique string request types collected");
+        }
+        String[] result = new String[mRequestCounts.keySet().size()];
+        mRequestCounts.keySet().toArray(result);
+        return result;
+    }
+
+    /**
+     * Reruns the unique, deduped, request classtype counts for logging.
+     * @return a string array for deduped classtype counts
+     */
+    public int[] getUniqueRequestCounts() {
+        if (mRequestCounts.isEmpty()) {
+            Log.w(TAG, "There are no unique string request type counts collected");
+        }
+        return mRequestCounts.values().stream().mapToInt(Integer::intValue).toArray();
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java
index 76fd478..9a88255 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderSessionMetric.java
@@ -61,6 +61,18 @@
     }
 
     /**
+     * Collects the framework only exception encountered in a candidate flow.
+     * @param exceptionType the string, cut to desired length, of the exception type
+     */
+    public void collectCandidateFrameworkException(String exceptionType) {
+        try {
+            mCandidatePhasePerProviderMetric.setFrameworkException(exceptionType);
+        } catch (Exception e) {
+            Log.w(TAG, "Unexpected error during metric logging: " + e);
+        }
+    }
+
+    /**
      * Used to collect metrics at the update stage when a candidate provider gives back an update.
      *
      * @param isFailureStatus indicates the candidate provider sent back a terminated response
diff --git a/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java b/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java
index a12a694..b1e6a4c 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/ProviderStatusForMetrics.java
@@ -38,7 +38,7 @@
     private final int mInnerMetricCode;
 
     ProviderStatusForMetrics(int innerMetricCode) {
-        this.mInnerMetricCode = innerMetricCode;
+        mInnerMetricCode = innerMetricCode;
     }
 
     /**
@@ -47,6 +47,6 @@
      * @return a code corresponding to the west world metric name
      */
     public int getMetricCode() {
-        return this.mInnerMetricCode;
+        return mInnerMetricCode;
     }
 }
diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
index 10bf56c..547c09a 100644
--- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
+++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java
@@ -16,10 +16,12 @@
 
 package com.android.server.credentials.metrics;
 
+import static com.android.server.credentials.MetricUtilities.DELTA_CUT;
+import static com.android.server.credentials.MetricUtilities.generateMetricKey;
 import static com.android.server.credentials.MetricUtilities.logApiCalledCandidatePhase;
 import static com.android.server.credentials.MetricUtilities.logApiCalledFinalPhase;
 
-import android.annotation.NonNull;
+import android.credentials.GetCredentialRequest;
 import android.credentials.ui.UserSelectionDialogResult;
 import android.os.IBinder;
 import android.util.Log;
@@ -27,6 +29,7 @@
 import com.android.server.credentials.ProviderSession;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -48,7 +51,6 @@
     protected final ChosenProviderFinalPhaseMetric
             mChosenProviderFinalPhaseMetric = new ChosenProviderFinalPhaseMetric();
     // TODO(b/271135048) - Replace this with a new atom per each browsing emit (V4)
-    @NonNull
     protected List<CandidateBrowsingPhaseMetric> mCandidateBrowsingPhaseMetric = new ArrayList<>();
 
     public RequestSessionMetric() {
@@ -161,16 +163,32 @@
         }
     }
 
+    // Used by get flows to generate the unique request count maps
+    private Map<String, Integer> getRequestCountMap(GetCredentialRequest request) {
+        Map<String, Integer> uniqueRequestCounts = new LinkedHashMap<>();
+        try {
+            request.getCredentialOptions().forEach(option -> {
+                String optionKey = generateMetricKey(option.getType(), DELTA_CUT);
+                if (!uniqueRequestCounts.containsKey(optionKey)) {
+                    uniqueRequestCounts.put(optionKey, 0);
+                }
+                uniqueRequestCounts.put(optionKey, uniqueRequestCounts.get(optionKey) + 1);
+            });
+        } catch (Exception e) {
+            Log.w(TAG, "Unexpected error during get request metric logging: " + e);
+        }
+        return uniqueRequestCounts;
+    }
+
     /**
      * Collects initializations for Get flow metrics.
      *
-     * @param requestClassTypeCount the number of class types in the request
-     * @param origin indicates if an origin was passed in or not
+     * @param request the get credential request containing information to parse for metrics
      */
-    public void collectGetFlowInitialMetricInfo(int requestClassTypeCount, boolean origin) {
+    public void collectGetFlowInitialMetricInfo(GetCredentialRequest request) {
         try {
-            mInitialPhaseMetric.setCountRequestClassType(requestClassTypeCount);
-            mInitialPhaseMetric.setOriginSpecified(origin);
+            mInitialPhaseMetric.setOriginSpecified(request.getOrigin() != null);
+            mInitialPhaseMetric.setRequestCounts(getRequestCountMap(request));
         } catch (Exception e) {
             Log.w(TAG, "Unexpected error during metric logging: " + e);
         }
@@ -306,7 +324,7 @@
      */
     public void logCandidatePhaseMetrics(Map<String, ProviderSession> providers) {
         try {
-            logApiCalledCandidatePhase(providers, ++mSequenceCounter);
+            logApiCalledCandidatePhase(providers, ++mSequenceCounter, mInitialPhaseMetric);
         } 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/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index ab8f3f2..d12741a 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -211,8 +211,7 @@
     @Test
     public void testStartUser_foreground() {
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
-        verify(mInjector.getWindowManager()).startFreezingScreen(anyInt(), anyInt());
-        verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+        verify(mInjector, never()).dismissUserSwitchingDialog(any());
         verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
         verify(mInjector.getWindowManager()).setSwitchingUser(true);
         verify(mInjector).clearAllLockedTasks(anyString());
@@ -224,7 +223,8 @@
     public void testStartUser_background() {
         boolean started = mUserController.startUser(TEST_USER_ID, USER_START_MODE_BACKGROUND);
         assertWithMessage("startUser(%s, foreground=false)", TEST_USER_ID).that(started).isTrue();
-        verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
+        verify(mInjector, never()).showUserSwitchingDialog(
+                any(), any(), anyString(), anyString(), any());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         verify(mInjector, never()).clearAllLockedTasks(anyString());
         startBackgroundUserAssertions();
@@ -276,7 +276,8 @@
         assertWithMessage("startUserOnDisplay(%s, %s)", TEST_USER_ID, 42).that(started).isTrue();
         verifyUserAssignedToDisplay(TEST_USER_ID, 42);
 
-        verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
+        verify(mInjector, never()).showUserSwitchingDialog(
+                any(), any(), anyString(), anyString(), any());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         verify(mInjector, never()).clearAllLockedTasks(anyString());
         startBackgroundUserAssertions();
@@ -288,8 +289,9 @@
                 /* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
 
         mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
-        verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
-        verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+        verify(mInjector, never()).showUserSwitchingDialog(
+                any(), any(), anyString(), anyString(), any());
+        verify(mInjector, never()).dismissUserSwitchingDialog(any());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         startForegroundUserAssertions();
     }
@@ -310,7 +312,8 @@
         // Make sure no intents have been fired for pre-created users.
         assertTrue(mInjector.mSentIntents.isEmpty());
 
-        verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
+        verify(mInjector, never()).showUserSwitchingDialog(
+                any(), any(), anyString(), anyString(), any());
         verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
         verify(mInjector, never()).clearAllLockedTasks(anyString());
 
@@ -442,7 +445,7 @@
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
         verify(mInjector, times(0)).dismissKeyguard(any());
-        verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
+        verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
         verifySystemUserVisibilityChangesNeverNotified();
     }
@@ -463,7 +466,7 @@
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
         verify(mInjector, times(1)).dismissKeyguard(any());
-        verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
+        verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
         verifySystemUserVisibilityChangesNeverNotified();
     }
@@ -483,7 +486,7 @@
         mInjector.mHandler.clearAllRecordedMessages();
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
+        verify(mInjector, never()).dismissUserSwitchingDialog(any());
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
     }
 
@@ -985,8 +988,7 @@
         mInjector.mHandler.clearAllRecordedMessages();
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector.getWindowManager(), times(expectedNumberOfCalls))
-                .stopFreezingScreen();
+        verify(mInjector, times(expectedNumberOfCalls)).dismissUserSwitchingDialog(any());
         continueUserSwitchAssertions(oldUserId, newUserId, expectOldUserStopping);
     }
 
@@ -1189,6 +1191,22 @@
         }
 
         @Override
+        void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
+                String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
+                Runnable onShown) {
+            if (onShown != null) {
+                onShown.run();
+            }
+        }
+
+        @Override
+        void dismissUserSwitchingDialog(Runnable onDismissed) {
+            if (onDismissed != null) {
+                onDismissed.run();
+            }
+        }
+
+        @Override
         protected LockPatternUtils getLockPatternUtils() {
             return mLockPatternUtilsMock;
         }
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
index 3ed95eb..bacf256 100644
--- 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
@@ -45,16 +45,22 @@
     @Test
     public void createPhoneAccount_success() {
         final PhoneAccount phoneAccount = mSyncConnectionService.createPhoneAccount(
-                "com.google.test", "Test App");
+                new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
+                associationId= */
+                        0, "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");
+                new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
+                associationId= */
+                        0, "com.google.test"), "Test App");
         final PhoneAccount phoneAccount2 = mSyncConnectionService.createPhoneAccount(
-                "com.google.test", "Test App #2");
+                new CallMetadataSyncConnectionService.PhoneAccountHandleIdentifier(/*
+                associationId= */
+                        0, "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/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index dd9f3cb..94f52bb 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -358,7 +358,7 @@
     private static final int NOTIFICATION_LOCATION_UNKNOWN = 0;
 
     private static final String VALID_CONVO_SHORTCUT_ID = "shortcut";
-
+    private static final String SEARCH_SELECTOR_PKG = "searchSelector";
     @Mock
     private NotificationListeners mListeners;
     @Mock
@@ -549,6 +549,10 @@
         // apps allowed as convos
         mService.setStringArrayResourceValue(PKG_O);
 
+        TestableResources tr = mContext.getOrCreateTestableResources();
+        tr.addOverride(com.android.internal.R.string.config_defaultSearchSelectorPackageName,
+                SEARCH_SELECTOR_PKG);
+
         mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper()));
         mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient,
                 mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr,
@@ -10636,6 +10640,34 @@
     }
 
     @Test
+    public void fixSystemNotification_defaultSearchSelectior_withOnGoingFlag_nondismissible()
+            throws Exception {
+        final ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = SEARCH_SELECTOR_PKG;
+        ai.uid = mUid;
+        ai.flags |= ApplicationInfo.FLAG_SYSTEM;
+
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(ai);
+        when(mAppOpsManager.checkOpNoThrow(
+                AppOpsManager.OP_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS, ai.uid,
+                ai.packageName)).thenReturn(AppOpsManager.MODE_IGNORED);
+        // Given: a notification from an app on the system partition has the flag
+        // FLAG_ONGOING_EVENT set
+        // feature flag: ALLOW_DISMISS_ONGOING is on
+        mTestFlagResolver.setFlagOverride(ALLOW_DISMISS_ONGOING, true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(true)
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be set
+        assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+    }
+
+    @Test
     public void fixCallNotification_withOnGoingFlag_shouldNotBeNonDismissible()
             throws Exception {
         // Given: a call notification has the flag FLAG_ONGOING_EVENT set
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/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 7abae18..f27c23b 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -7640,6 +7640,25 @@
         public static final String KEY_EMERGENCY_SCAN_TIMER_SEC_INT =
                 KEY_PREFIX + "emergency_scan_timer_sec_int";
 
+        /**
+         * The timer to wait for the call completion on the cellular network before attempting the
+         * call over Wi-Fi. On timer expiry, if emergency call on Wi-Fi is allowed and possible,
+         * telephony shall cancel the scan on the cellular network and place the call on Wi-Fi.
+         * If dialing over cellular network is ongoing when timer expires, dialing over Wi-Fi
+         * will be requested only when the ongoing dialing fails. If emergency call on Wi-Fi is not
+         * possible, then domain selection continues to try dialing from the radio and the timer
+         * remains expired. Later when calling over Wi-Fi is possible and dialing over cellular
+         * networks fails, calling over Wi-Fi will be requested. The timer shall be restarted from
+         * initial state if calling over Wi-Fi fails.
+         * If this value is set to {@link #REDIAL_TIMER_DISABLED}, then the timer will never be
+         * started.
+         *
+         * The default value for the timer is {@link #REDIAL_TIMER_DISABLED}.
+         * @hide
+         */
+        public static final String KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT =
+                KEY_PREFIX + "maximum_cellular_search_timer_sec_int";
+
         /** @hide */
         @IntDef(prefix = "SCAN_TYPE_",
             value = {
@@ -7734,10 +7753,12 @@
                 KEY_PREFIX + "emergency_requires_volte_enabled_bool";
 
         /**
-         * This values indicates that the cross SIM redialing timer shall be disabled.
+         * This values indicates that the cross SIM redialing timer and maximum celluar search
+         * timer shall be disabled.
          *
          * @see #KEY_CROSS_STACK_REDIAL_TIMER_SEC_INT
          * @see #KEY_QUICK_CROSS_STACK_REDIAL_TIMER_SEC_INT
+         * @see #KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT
          * @hide
          */
         public static final int REDIAL_TIMER_DISABLED = 0;
@@ -7841,6 +7862,7 @@
             defaults.putInt(KEY_EMERGENCY_VOWIFI_REQUIRES_CONDITION_INT, VOWIFI_REQUIRES_NONE);
             defaults.putInt(KEY_MAXIMUM_NUMBER_OF_EMERGENCY_TRIES_OVER_VOWIFI_INT, 1);
             defaults.putInt(KEY_EMERGENCY_SCAN_TIMER_SEC_INT, 10);
+            defaults.putInt(KEY_MAXIMUM_CELLULAR_SEARCH_TIMER_SEC_INT, REDIAL_TIMER_DISABLED);
             defaults.putInt(KEY_EMERGENCY_NETWORK_SCAN_TYPE_INT, SCAN_TYPE_NO_PREFERENCE);
             defaults.putInt(KEY_EMERGENCY_CALL_SETUP_TIMER_ON_CURRENT_NETWORK_SEC_INT, 0);
             defaults.putBoolean(KEY_EMERGENCY_REQUIRES_IMS_REGISTRATION_BOOL, false);
diff --git a/telephony/java/android/telephony/UiccCardInfo.java b/telephony/java/android/telephony/UiccCardInfo.java
index e5e8f0a..6c069d4 100644
--- a/telephony/java/android/telephony/UiccCardInfo.java
+++ b/telephony/java/android/telephony/UiccCardInfo.java
@@ -166,7 +166,7 @@
                 + " Please Use UiccPortInfo API instead");
         }
         //always return ICCID from first port.
-        return getPorts().stream().findFirst().get().getIccId();
+        return mPortList.isEmpty() ? null : mPortList.get(0).getIccId();
     }
 
     /**
diff --git a/telephony/java/android/telephony/UiccSlotInfo.java b/telephony/java/android/telephony/UiccSlotInfo.java
index 1863a03b..dda7349 100644
--- a/telephony/java/android/telephony/UiccSlotInfo.java
+++ b/telephony/java/android/telephony/UiccSlotInfo.java
@@ -139,14 +139,16 @@
     public UiccSlotInfo(boolean isEuicc, String cardId,
             @CardStateInfo int cardStateInfo, boolean isExtendedApduSupported,
             boolean isRemovable, @NonNull List<UiccPortInfo> portList) {
-        this.mIsActive = portList.get(0).isActive();
         this.mIsEuicc = isEuicc;
         this.mCardId = cardId;
         this.mCardStateInfo = cardStateInfo;
-        this.mLogicalSlotIdx = portList.get(0).getLogicalSlotIndex();
         this.mIsExtendedApduSupported = isExtendedApduSupported;
         this.mIsRemovable = isRemovable;
         this.mPortList = portList;
+        this.mIsActive = !portList.isEmpty() && portList.get(0).isActive();
+        this.mLogicalSlotIdx = portList.isEmpty()
+                ? SubscriptionManager.INVALID_PHONE_INDEX
+                : portList.get(0).getLogicalSlotIndex();
     }
 
     /**
@@ -164,8 +166,7 @@
             throw new UnsupportedOperationException("getIsActive() is not supported by "
             + "UiccSlotInfo. Please Use UiccPortInfo API instead");
         }
-        //always return status from first port.
-        return getPorts().stream().findFirst().get().isActive();
+        return mIsActive;
     }
 
     public boolean getIsEuicc() {
@@ -202,9 +203,7 @@
             throw new UnsupportedOperationException("getLogicalSlotIdx() is not supported by "
                 + "UiccSlotInfo. Please use UiccPortInfo API instead");
         }
-        //always return logical slot index from first port.
-        //portList always have at least one element.
-        return getPorts().stream().findFirst().get().getLogicalSlotIndex();
+        return mLogicalSlotIdx;
     }
 
     /**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index a72c12d..c5a21a8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -250,7 +250,10 @@
             waitConditions = arrayOf(ConditionsFactory.hasPipWindow())
         )
 
-        wmHelper.StateSyncBuilder().withPipShown().waitForAndVerify()
+        wmHelper.StateSyncBuilder()
+            .withWindowSurfaceAppeared(this)
+            .withPipShown()
+            .waitForAndVerify()
     }
 
     /** Expand the PIP window back to full screen via intent and wait until the app is visible */