Merge "Fix exception when opening App info on work profile" into tm-qpr-dev
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
index 452bb0a..0620721 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java
@@ -19,6 +19,7 @@
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 
+import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.perftests.utils.ManualBenchmarkState;
@@ -86,6 +87,7 @@
         final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
         final InsetsState mOutInsetsState = new InsetsState();
         final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0];
+        final Rect mOutAttachedFrame = new Rect();
 
         TestWindow() {
             mLayoutParams.setTitle(TestWindow.class.getName());
@@ -104,7 +106,7 @@
                 long startTime = SystemClock.elapsedRealtimeNanos();
                 session.addToDisplay(this, mLayoutParams, View.VISIBLE,
                         Display.DEFAULT_DISPLAY, mRequestedVisibilities, inputChannel,
-                        mOutInsetsState, mOutControls);
+                        mOutInsetsState, mOutControls, mOutAttachedFrame);
                 final long elapsedTimeNsOfAdd = SystemClock.elapsedRealtimeNanos() - startTime;
                 state.addExtraResult("add", elapsedTimeNsOfAdd);
 
diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
index 2fcab59..78214dc 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java
@@ -385,6 +385,12 @@
      */
     public static final int REASON_ACTIVE_DEVICE_ADMIN = 324;
 
+    /**
+     * Media notification re-generate during transferring.
+     * @hide
+     */
+    public static final int REASON_MEDIA_NOTIFICATION_TRANSFER = 325;
+
     /** @hide The app requests out-out. */
     public static final int REASON_OPT_OUT_REQUESTED = 1000;
 
@@ -465,6 +471,7 @@
             REASON_DPO_PROTECTED_APP,
             REASON_DISALLOW_APPS_CONTROL,
             REASON_ACTIVE_DEVICE_ADMIN,
+            REASON_MEDIA_NOTIFICATION_TRANSFER,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ReasonCode {}
@@ -830,6 +837,8 @@
                 return "ACTIVE_DEVICE_ADMIN";
             case REASON_OPT_OUT_REQUESTED:
                 return "REASON_OPT_OUT_REQUESTED";
+            case REASON_MEDIA_NOTIFICATION_TRANSFER:
+                return "REASON_MEDIA_NOTIFICATION_TRANSFER";
             default:
                 return "(unknown:" + reasonCode + ")";
         }
diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
index c43c832..9b64edf 100644
--- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
+++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java
@@ -164,6 +164,13 @@
             @ElapsedRealtimeLong long elapsedRealtime);
 
     /**
+     * Puts the list of apps in the {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RARE}
+     * bucket.
+     * @param restoredApps the list of restored apps
+     */
+    void restoreAppsToRare(@NonNull Set<String> restoredApps, int userId);
+
+    /**
      * Put the specified app in the
      * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED}
      * bucket. If it has been used by the user recently, the restriction will delayed until an
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 9e3e355..5d9f335 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -23,6 +23,7 @@
 import static android.app.usage.UsageStatsManager.REASON_MAIN_PREDICTED;
 import static android.app.usage.UsageStatsManager.REASON_MAIN_TIMEOUT;
 import static android.app.usage.UsageStatsManager.REASON_MAIN_USAGE;
+import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_RESTORED;
 import static android.app.usage.UsageStatsManager.REASON_SUB_DEFAULT_APP_UPDATE;
 import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_SYSTEM_FLAG_BUGGY;
 import static android.app.usage.UsageStatsManager.REASON_SUB_FORCED_USER_FLAG_INTERACTION;
@@ -1605,6 +1606,26 @@
     }
 
     @Override
+    public void restoreAppsToRare(Set<String> restoredApps, int userId) {
+        final int reason = REASON_MAIN_DEFAULT | REASON_SUB_DEFAULT_APP_RESTORED;
+        final long nowElapsed = mInjector.elapsedRealtime();
+        for (String packageName : restoredApps) {
+            // If the package is not installed, don't allow the bucket to be set.
+            if (!mInjector.isPackageInstalled(packageName, 0, userId)) {
+                Slog.e(TAG, "Tried to restore bucket for uninstalled app: " + packageName);
+                continue;
+            }
+
+            final int standbyBucket = getAppStandbyBucket(packageName, userId, nowElapsed, false);
+            // Only update the standby bucket to RARE if the app is still in the NEVER bucket.
+            if (standbyBucket == STANDBY_BUCKET_NEVER) {
+                setAppStandbyBucket(packageName, userId, STANDBY_BUCKET_RARE, reason,
+                        nowElapsed, false);
+            }
+        }
+    }
+
+    @Override
     public void setAppStandbyBucket(@NonNull String packageName, int bucket, int userId,
             int callingUid, int callingPid) {
         setAppStandbyBuckets(
diff --git a/boot/Android.bp b/boot/Android.bp
index 5b265a5..3f14ebc 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -60,8 +60,8 @@
             module: "art-bootclasspath-fragment",
         },
         {
-            apex: "com.android.bluetooth",
-            module: "com.android.bluetooth-bootclasspath-fragment",
+            apex: "com.android.btservices",
+            module: "com.android.btservices-bootclasspath-fragment",
         },
         {
             apex: "com.android.conscrypt",
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 802458b..546efaa 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -274,11 +274,6 @@
     public static final boolean DEBUG_ORDER = false;
     private static final long MIN_TIME_BETWEEN_GCS = 5*1000;
     /**
-     * If the activity doesn't become idle in time, the timeout will ensure to apply the pending top
-     * process state.
-     */
-    private static final long PENDING_TOP_PROCESS_STATE_TIMEOUT = 1000;
-    /**
      * The delay to release the provider when it has no more references. It reduces the number of
      * transactions for acquiring and releasing provider if the client accesses the provider
      * frequently in a short time.
@@ -367,8 +362,6 @@
     private final AtomicInteger mNumLaunchingActivities = new AtomicInteger();
     @GuardedBy("mAppThread")
     private int mLastProcessState = PROCESS_STATE_UNKNOWN;
-    @GuardedBy("mAppThread")
-    private int mPendingProcessState = PROCESS_STATE_UNKNOWN;
     ArrayList<WeakReference<AssistStructure>> mLastAssistStructures = new ArrayList<>();
     private int mLastSessionId;
     final ArrayMap<IBinder, CreateServiceData> mServicesData = new ArrayMap<>();
@@ -2384,7 +2377,6 @@
             if (stopProfiling) {
                 mProfiler.stopProfiling();
             }
-            applyPendingProcessState();
             return false;
         }
     }
@@ -3438,16 +3430,7 @@
                 return;
             }
             mLastProcessState = processState;
-            // Defer the top state for VM to avoid aggressive JIT compilation affecting activity
-            // launch time.
-            if (processState == ActivityManager.PROCESS_STATE_TOP
-                    && mNumLaunchingActivities.get() > 0) {
-                mPendingProcessState = processState;
-                mH.postDelayed(this::applyPendingProcessState, PENDING_TOP_PROCESS_STATE_TIMEOUT);
-            } else {
-                mPendingProcessState = PROCESS_STATE_UNKNOWN;
-                updateVmProcessState(processState);
-            }
+            updateVmProcessState(processState);
             if (localLOGV) {
                 Slog.i(TAG, "******************* PROCESS STATE CHANGED TO: " + processState
                         + (fromIpc ? " (from ipc" : ""));
@@ -3465,20 +3448,6 @@
         VMRuntime.getRuntime().updateProcessState(state);
     }
 
-    private void applyPendingProcessState() {
-        synchronized (mAppThread) {
-            if (mPendingProcessState == PROCESS_STATE_UNKNOWN) {
-                return;
-            }
-            final int pendingState = mPendingProcessState;
-            mPendingProcessState = PROCESS_STATE_UNKNOWN;
-            // Only apply the pending state if the last state doesn't change.
-            if (pendingState == mLastProcessState) {
-                updateVmProcessState(pendingState);
-            }
-        }
-    }
-
     @Override
     public void countLaunchingActivities(int num) {
         mNumLaunchingActivities.getAndAdd(num);
diff --git a/core/java/android/app/usage/UsageStats.java b/core/java/android/app/usage/UsageStats.java
index d61abc6..e213c93 100644
--- a/core/java/android/app/usage/UsageStats.java
+++ b/core/java/android/app/usage/UsageStats.java
@@ -293,6 +293,17 @@
     }
 
     /**
+     * Returns the last time the package was used - defined by the latest of
+     * mLastTimeUsed, mLastTimeVisible, mLastTimeForegroundServiceUsed, or mLastTimeComponentUsed.
+     * @hide
+     */
+    public long getLastTimePackageUsed() {
+        return Math.max(mLastTimeUsed,
+                        Math.max(mLastTimeVisible,
+                                 Math.max(mLastTimeForegroundServiceUsed, mLastTimeComponentUsed)));
+    }
+
+    /**
      * Returns the number of times the app was launched as an activity from outside of the app.
      * Excludes intra-app activity transitions.
      * @hide
diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java
index c013fcd..1dfc7d4 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -220,6 +220,11 @@
      */
     public static final int REASON_SUB_DEFAULT_APP_UPDATE = 0x0001;
     /**
+     * The app was restored.
+     * @hide
+     */
+    public static final int REASON_SUB_DEFAULT_APP_RESTORED = 0x0002;
+    /**
      * The app was interacted with in some way by the system.
      * @hide
      */
@@ -1209,6 +1214,9 @@
                     case REASON_SUB_DEFAULT_APP_UPDATE:
                         sb.append("-au");
                         break;
+                    case REASON_SUB_DEFAULT_APP_RESTORED:
+                        sb.append("-ar");
+                        break;
                 }
                 break;
             case REASON_MAIN_FORCED_BY_SYSTEM:
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index f7f0235..93748f8 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -55,6 +55,14 @@
 
     private final boolean mSelfManaged;
     private final boolean mNotifyOnDeviceNearby;
+
+    /**
+     * Indicates that the association has been revoked (removed), but we keep the association
+     * record for final clean up (e.g. removing the app from the list of the role holders).
+     *
+     * @see CompanionDeviceManager#disassociate(int)
+     */
+    private final boolean mRevoked;
     private final long mTimeApprovedMs;
     /**
      * A long value indicates the last time connected reported by selfManaged devices
@@ -71,7 +79,7 @@
     public AssociationInfo(int id, @UserIdInt int userId, @NonNull String packageName,
             @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
             @Nullable String deviceProfile, boolean selfManaged, boolean notifyOnDeviceNearby,
-            long timeApprovedMs, long lastTimeConnectedMs) {
+            boolean revoked, long timeApprovedMs, long lastTimeConnectedMs) {
         if (id <= 0) {
             throw new IllegalArgumentException("Association ID should be greater than 0");
         }
@@ -91,6 +99,7 @@
 
         mSelfManaged = selfManaged;
         mNotifyOnDeviceNearby = notifyOnDeviceNearby;
+        mRevoked = revoked;
         mTimeApprovedMs = timeApprovedMs;
         mLastTimeConnectedMs = lastTimeConnectedMs;
     }
@@ -176,6 +185,14 @@
     }
 
     /**
+     * @return if the association has been revoked (removed).
+     * @hide
+     */
+    public boolean isRevoked() {
+        return mRevoked;
+    }
+
+    /**
      * @return the last time self reported disconnected for selfManaged only.
      * @hide
      */
@@ -244,6 +261,7 @@
                 + ", mDeviceProfile='" + mDeviceProfile + '\''
                 + ", mSelfManaged=" + mSelfManaged
                 + ", mNotifyOnDeviceNearby=" + mNotifyOnDeviceNearby
+                + ", mRevoked=" + mRevoked
                 + ", mTimeApprovedMs=" + new Date(mTimeApprovedMs)
                 + ", mLastTimeConnectedMs=" + (
                     mLastTimeConnectedMs == Long.MAX_VALUE
@@ -260,6 +278,7 @@
                 && mUserId == that.mUserId
                 && mSelfManaged == that.mSelfManaged
                 && mNotifyOnDeviceNearby == that.mNotifyOnDeviceNearby
+                && mRevoked == that.mRevoked
                 && mTimeApprovedMs == that.mTimeApprovedMs
                 && mLastTimeConnectedMs == that.mLastTimeConnectedMs
                 && Objects.equals(mPackageName, that.mPackageName)
@@ -271,7 +290,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(mId, mUserId, mPackageName, mDeviceMacAddress, mDisplayName,
-                mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mTimeApprovedMs,
+                mDeviceProfile, mSelfManaged, mNotifyOnDeviceNearby, mRevoked, mTimeApprovedMs,
                 mLastTimeConnectedMs);
     }
 
@@ -293,6 +312,7 @@
 
         dest.writeBoolean(mSelfManaged);
         dest.writeBoolean(mNotifyOnDeviceNearby);
+        dest.writeBoolean(mRevoked);
         dest.writeLong(mTimeApprovedMs);
         dest.writeLong(mLastTimeConnectedMs);
     }
@@ -309,6 +329,7 @@
 
         mSelfManaged = in.readBoolean();
         mNotifyOnDeviceNearby = in.readBoolean();
+        mRevoked = in.readBoolean();
         mTimeApprovedMs = in.readLong();
         mLastTimeConnectedMs = in.readLong();
     }
@@ -352,11 +373,13 @@
         @NonNull
         private final AssociationInfo mOriginalInfo;
         private boolean mNotifyOnDeviceNearby;
+        private boolean mRevoked;
         private long mLastTimeConnectedMs;
 
         private Builder(@NonNull AssociationInfo info) {
             mOriginalInfo = info;
             mNotifyOnDeviceNearby = info.mNotifyOnDeviceNearby;
+            mRevoked = info.mRevoked;
             mLastTimeConnectedMs = info.mLastTimeConnectedMs;
         }
 
@@ -388,6 +411,17 @@
         }
 
         /**
+         * Should only be used by the CompanionDeviceManagerService.
+         * @hide
+         */
+        @Override
+        @NonNull
+        public Builder setRevoked(boolean revoked) {
+            mRevoked = revoked;
+            return this;
+        }
+
+        /**
          * @hide
          */
         @NonNull
@@ -401,6 +435,7 @@
                     mOriginalInfo.mDeviceProfile,
                     mOriginalInfo.mSelfManaged,
                     mNotifyOnDeviceNearby,
+                    mRevoked,
                     mOriginalInfo.mTimeApprovedMs,
                     mLastTimeConnectedMs
             );
@@ -433,5 +468,12 @@
          */
         @NonNull
         Builder setLastTimeConnected(long lastTimeConnectedMs);
+
+        /**
+         * Should only be used by the CompanionDeviceManagerService.
+         * @hide
+         */
+        @NonNull
+        Builder setRevoked(boolean revoked);
     }
 }
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 20a4fdf..10d6f2d 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -542,14 +542,17 @@
 
                 int minVer = DEFAULT_MIN_SDK_VERSION;
                 String minCode = null;
+                boolean minAssigned = false;
                 int targetVer = DEFAULT_TARGET_SDK_VERSION;
                 String targetCode = null;
 
                 if (!TextUtils.isEmpty(minSdkVersionString)) {
                     try {
                         minVer = Integer.parseInt(minSdkVersionString);
+                        minAssigned = true;
                     } catch (NumberFormatException ignored) {
                         minCode = minSdkVersionString;
+                        minAssigned = !TextUtils.isEmpty(minCode);
                     }
                 }
 
@@ -558,7 +561,7 @@
                         targetVer = Integer.parseInt(targetSdkVersionString);
                     } catch (NumberFormatException ignored) {
                         targetCode = targetSdkVersionString;
-                        if (minCode == null) {
+                        if (!minAssigned) {
                             minCode = targetCode;
                         }
                     }
diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java
index 439c639..608e34b 100644
--- a/core/java/android/content/res/CompatibilityInfo.java
+++ b/core/java/android/content/res/CompatibilityInfo.java
@@ -420,7 +420,10 @@
          * Translate a Rect in screen coordinates into the app window's coordinates.
          */
         @UnsupportedAppUsage
-        public void translateRectInScreenToAppWindow(Rect rect) {
+        public void translateRectInScreenToAppWindow(@Nullable Rect rect) {
+            if (rect == null) {
+                return;
+            }
             rect.scale(applicationInvertedScale);
         }
 
diff --git a/core/java/android/debug/AdbManagerInternal.java b/core/java/android/debug/AdbManagerInternal.java
index d730129..e448706 100644
--- a/core/java/android/debug/AdbManagerInternal.java
+++ b/core/java/android/debug/AdbManagerInternal.java
@@ -55,6 +55,12 @@
     public abstract File getAdbTempKeysFile();
 
     /**
+     * Notify the AdbManager that the key files have changed and any in-memory state should be
+     * reloaded.
+     */
+    public abstract void notifyKeyFilesUpdated();
+
+    /**
      * Starts adbd for a transport.
      */
     public abstract void startAdbdForTransport(byte transportType);
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index b505395..8bc11cb 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1448,5 +1448,15 @@
          * @hide
          */
         String KEY_HIGH_REFRESH_RATE_BLACKLIST = "high_refresh_rate_blacklist";
+
+        /**
+         * Key for the brightness throttling data as a String formatted:
+         * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>]
+         * Where the latter part is repeated for each throttling level, and the entirety is repeated
+         * for each display, separated by a semicolon.
+         * For example:
+         * 123,1,critical,0.8;456,2,moderate,0.9,critical,0.7
+         */
+        String KEY_BRIGHTNESS_THROTTLING_DATA = "brightness_throttling_data";
     }
 }
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index 3a042a5..e8e4fc9 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -26,7 +26,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -173,38 +172,63 @@
         }
     }
 
-    void apply(@NonNull Chunk chunk) {
+    void apply(Chunk chunk) {
+        List<ProgramSelector.Identifier> removedList = new ArrayList<>();
+        List<ProgramSelector.Identifier> changedList = new ArrayList<>();
+        List<ProgramList.ListCallback> listCallbacksCopied;
+        List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>();
         synchronized (mLock) {
             if (mIsClosed) return;
 
             mIsComplete = false;
+            listCallbacksCopied = new ArrayList<>(mListCallbacks);
 
             if (chunk.isPurge()) {
-                new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id));
+                for (ProgramSelector.Identifier id : mPrograms.keySet()) {
+                    removeLocked(id, removedList);
+                }
             }
 
-            chunk.getRemoved().stream().forEach(id -> removeLocked(id));
-            chunk.getModified().stream().forEach(info -> putLocked(info));
+            chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList));
+            chunk.getModified().stream().forEach(info -> putLocked(info, changedList));
 
             if (chunk.isComplete()) {
                 mIsComplete = true;
-                mOnCompleteListeners.forEach(cb -> cb.onComplete());
+                onCompleteListenersCopied = new ArrayList<>(mOnCompleteListeners);
+            }
+        }
+
+        for (int i = 0; i < removedList.size(); i++) {
+            for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
+                listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i));
+            }
+        }
+        for (int i = 0; i < changedList.size(); i++) {
+            for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
+                listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i));
+            }
+        }
+        if (chunk.isComplete()) {
+            for (int cbIndex = 0; cbIndex < onCompleteListenersCopied.size(); cbIndex++) {
+                onCompleteListenersCopied.get(cbIndex).onComplete();
             }
         }
     }
 
-    private void putLocked(@NonNull RadioManager.ProgramInfo value) {
+    private void putLocked(RadioManager.ProgramInfo value,
+            List<ProgramSelector.Identifier> changedIdentifierList) {
         ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
         mPrograms.put(Objects.requireNonNull(key), value);
         ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
-        mListCallbacks.forEach(cb -> cb.onItemChanged(sel));
+        changedIdentifierList.add(sel);
     }
 
-    private void removeLocked(@NonNull ProgramSelector.Identifier key) {
+    private void removeLocked(ProgramSelector.Identifier key,
+            List<ProgramSelector.Identifier> removedIdentifierList) {
         RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
         if (removed == null) return;
         ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
-        mListCallbacks.forEach(cb -> cb.onItemRemoved(sel));
+        removedIdentifierList.add(sel);
     }
 
     /**
diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING
index 22ddbcc..c70f1f0 100644
--- a/core/java/android/os/TEST_MAPPING
+++ b/core/java/android/os/TEST_MAPPING
@@ -59,6 +59,18 @@
     },
     {
       "file_patterns": [
+        "Parcel\\.java",
+        "[^/]*Bundle[^/]*\\.java"
+      ],
+      "name": "FrameworksMockingCoreTests",
+      "options": [
+        { "include-filter":  "android.os.BundleRecyclingTest"},
+        { "exclude-annotation": "androidx.test.filters.FlakyTest" },
+        { "exclude-annotation": "org.junit.Ignore" }
+      ]
+    },
+    {
+      "file_patterns": [
         "BatteryUsageStats[^/]*\\.java",
         "PowerComponents\\.java",
         "[^/]*BatteryConsumer[^/]*\\.java"
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 9679a6a..1df7dbc 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -24,7 +24,6 @@
 import static android.graphics.Matrix.MSKEW_Y;
 import static android.view.SurfaceControl.METADATA_WINDOW_TYPE;
 import static android.view.View.SYSTEM_UI_FLAG_VISIBLE;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
 
 import android.animation.AnimationHandler;
@@ -41,7 +40,6 @@
 import android.app.WallpaperColors;
 import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
-import android.app.WindowConfiguration;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
@@ -260,8 +258,6 @@
         private final Point mLastSurfaceSize = new Point();
         private final Matrix mTmpMatrix = new Matrix();
         private final float[] mTmpValues = new float[9];
-        private final WindowLayout mWindowLayout = new WindowLayout();
-        private final Rect mTempRect = new Rect();
 
         final WindowManager.LayoutParams mLayout
                 = new WindowManager.LayoutParams();
@@ -1100,8 +1096,7 @@
                             | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
 
                     final Configuration config = mMergedConfiguration.getMergedConfiguration();
-                    final WindowConfiguration winConfig = config.windowConfiguration;
-                    final Rect maxBounds = winConfig.getMaxBounds();
+                    final Rect maxBounds = config.windowConfiguration.getMaxBounds();
                     if (myWidth == ViewGroup.LayoutParams.MATCH_PARENT
                             && myHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
                         mLayout.width = myWidth;
@@ -1139,7 +1134,7 @@
 
                         if (mSession.addToDisplay(mWindow, mLayout, View.VISIBLE,
                                 mDisplay.getDisplayId(), mRequestedVisibilities, inputChannel,
-                                mInsetsState, mTempControls) < 0) {
+                                mInsetsState, mTempControls, new Rect()) < 0) {
                             Log.w(TAG, "Failed to add window while updating wallpaper surface.");
                             return;
                         }
@@ -1158,29 +1153,9 @@
                     } else {
                         mLayout.surfaceInsets.set(0, 0, 0, 0);
                     }
-
-                    int relayoutResult = 0;
-                    if (LOCAL_LAYOUT) {
-                        if (!mSurfaceControl.isValid()) {
-                            relayoutResult = mSession.updateVisibility(mWindow, mLayout,
-                                    View.VISIBLE, mMergedConfiguration, mSurfaceControl,
-                                    mInsetsState, mTempControls);
-                        }
-
-                        final Rect displayCutoutSafe = mTempRect;
-                        mInsetsState.getDisplayCutoutSafe(displayCutoutSafe);
-                        mWindowLayout.computeFrames(mLayout, mInsetsState, displayCutoutSafe,
-                                winConfig.getBounds(), winConfig.getWindowingMode(), mWidth,
-                                mHeight, mRequestedVisibilities, null /* attachedWindowFrame */,
-                                1f /* compatScale */, mWinFrames);
-
-                        mSession.updateLayout(mWindow, mLayout, 0 /* flags */, mWinFrames, mWidth,
-                                mHeight);
-                    } else {
-                        relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight,
-                                View.VISIBLE, 0, mWinFrames, mMergedConfiguration,
-                                mSurfaceControl, mInsetsState, mTempControls, mSyncSeqIdBundle);
-                    }
+                    final int relayoutResult = mSession.relayout(mWindow, mLayout, mWidth, mHeight,
+                            View.VISIBLE, 0, mWinFrames, mMergedConfiguration, mSurfaceControl,
+                            mInsetsState, mTempControls, mSyncSeqIdBundle);
 
                     final int transformHint = SurfaceControl.rotationToBufferTransform(
                             (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
@@ -1229,7 +1204,7 @@
                             null /* ignoringVisibilityState */, config.isScreenRound(),
                             false /* alwaysConsumeSystemBars */, mLayout.softInputMode,
                             mLayout.flags, SYSTEM_UI_FLAG_VISIBLE, mLayout.type,
-                            winConfig.getWindowingMode(), null /* typeSideMap */);
+                            config.windowConfiguration.getWindowingMode(), null /* typeSideMap */);
 
                     if (!fixedSize) {
                         final Rect padding = mIWallpaperEngine.mDisplayPadding;
diff --git a/core/java/android/view/IRemoteAnimationRunner.aidl b/core/java/android/view/IRemoteAnimationRunner.aidl
index 1f64fb8..1981c9d 100644
--- a/core/java/android/view/IRemoteAnimationRunner.aidl
+++ b/core/java/android/view/IRemoteAnimationRunner.aidl
@@ -46,5 +46,5 @@
      * won't have any effect anymore.
      */
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
-    void onAnimationCancelled();
+    void onAnimationCancelled(boolean isKeyguardOccluded);
 }
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 649accd..ef57b1d 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -50,13 +50,15 @@
     int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, in InsetsVisibilities requestedVisibilities,
             out InputChannel outInputChannel, out InsetsState insetsState,
-            out InsetsSourceControl[] activeControls);
+            out InsetsSourceControl[] activeControls, out Rect attachedFrame);
     int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs,
             in int viewVisibility, in int layerStackId, in int userId,
             in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel,
-            out InsetsState insetsState, out InsetsSourceControl[] activeControls);
+            out InsetsState insetsState, out InsetsSourceControl[] activeControls,
+            out Rect attachedFrame);
     int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs,
-            in int viewVisibility, in int layerStackId, out InsetsState insetsState);
+            in int viewVisibility, in int layerStackId, out InsetsState insetsState,
+            out Rect attachedFrame);
     @UnsupportedAppUsage
     void remove(IWindow window);
 
@@ -107,41 +109,6 @@
             out InsetsState insetsState, out InsetsSourceControl[] activeControls,
             out Bundle bundle);
 
-    /**
-     * Changes the view visibility and the attributes of a window. This should only be called when
-     * the visibility of the root view is changed. This returns a valid surface if the root view is
-     * visible. This also returns the latest information for the caller to compute its window frame.
-     *
-     * @param window The window being updated.
-     * @param attrs If non-null, new attributes to apply to the window.
-     * @param viewVisibility Window root view's visibility.
-     * @param outMergedConfiguration New config container that holds global, override and merged
-     * config for window, if it is now becoming visible and the merged configuration has changed
-     * since it was last displayed.
-     * @param outSurfaceControl Object in which is placed the new display surface.
-     * @param outInsetsState The current insets state in the system.
-     * @param outActiveControls The insets source controls for the caller to override the insets
-     * state in the system.
-     *
-     * @return int Result flags: {@link WindowManagerGlobal#RELAYOUT_FIRST_TIME}.
-     */
-    int updateVisibility(IWindow window, in WindowManager.LayoutParams attrs, int viewVisibility,
-            out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl,
-            out InsetsState outInsetsState, out InsetsSourceControl[] outActiveControls);
-
-    /**
-     * Reports the layout results and the attributes of a window to the server.
-     *
-     * @param window The window being reported.
-     * @param attrs If non-null, new attributes to apply to the window.
-     * @param flags Request flags: {@link WindowManagerGlobal#RELAYOUT_INSETS_PENDING}.
-     * @param clientFrames the window frames computed by the client.
-     * @param requestedWidth The width the window wants to be.
-     * @param requestedHeight The height the window wants to be.
-     */
-    oneway void updateLayout(IWindow window, in WindowManager.LayoutParams attrs, int flags,
-            in ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight);
-
     /*
      * Notify the window manager that an application is relaunching and
      * windows should be prepared for replacement.
@@ -384,4 +351,9 @@
      * Clears a touchable region set by {@link #setInsets}.
      */
     void clearTouchableRegion(IWindow window);
+
+    /**
+     * Returns whether this window needs to cancel draw and retry later.
+     */
+    boolean cancelDraw(IWindow window);
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f197631..9a3957c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -60,13 +60,11 @@
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
 import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW;
-import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
-import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_APPEARANCE_CONTROLLED;
@@ -77,13 +75,12 @@
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
+import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
@@ -566,8 +563,6 @@
 
     private final WindowLayout mWindowLayout;
 
-    private ViewRootImpl mParentViewRoot;
-
     // This is used to reduce the race between window focus changes being dispatched from
     // the window manager and input events coming through the input system.
     @GuardedBy("this")
@@ -601,6 +596,14 @@
      */
     private boolean mSyncBuffer = false;
 
+    /**
+     * Flag to determine whether the client needs to check with WMS if it can draw. WMS will notify
+     * the client that it can't draw if we're still in the middle of a sync set that includes this
+     * window. Once the sync is complete, the window can resume drawing. This is to ensure we don't
+     * deadlock the client by trying to request draws when there may not be any buffers available.
+     */
+    private boolean mCheckIfCanDraw = false;
+
     int mSyncSeqId = 0;
     int mLastSyncSeqId = 0;
 
@@ -1187,7 +1190,6 @@
                 if (panelParentView != null) {
                     mAttachInfo.mPanelParentWindowToken
                             = panelParentView.getApplicationWindowToken();
-                    mParentViewRoot = panelParentView.getViewRootImpl();
                 }
                 mAdded = true;
                 int res; /* = WindowManagerImpl.ADD_OKAY; */
@@ -1218,14 +1220,21 @@
                     collectViewAttributes();
                     adjustLayoutParamsForCompatibility(mWindowAttributes);
                     controlInsetsForCompatibility(mWindowAttributes);
+
+                    Rect attachedFrame = new Rect();
                     res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                             getHostVisibility(), mDisplay.getDisplayId(), userId,
                             mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
-                            mTempControls);
+                            mTempControls, attachedFrame);
+                    if (!attachedFrame.isValid()) {
+                        attachedFrame = null;
+                    }
                     if (mTranslator != null) {
                         mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
                         mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
+                        mTranslator.translateRectInScreenToAppWindow(attachedFrame);
                     }
+                    mTmpFrames.attachedFrame = attachedFrame;
                 } catch (RemoteException e) {
                     mAdded = false;
                     mView = null;
@@ -1252,8 +1261,8 @@
                 mWindowLayout.computeFrames(mWindowAttributes, state,
                         displayCutoutSafe, winConfig.getBounds(), winConfig.getWindowingMode(),
                         UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH,
-                        mInsetsController.getRequestedVisibilities(),
-                        getAttachedWindowFrame(), 1f /* compactScale */, mTmpFrames);
+                        mInsetsController.getRequestedVisibilities(), 1f /* compactScale */,
+                        mTmpFrames);
                 setFrame(mTmpFrames.frame);
                 registerBackCallbackOnWindow();
                 if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
@@ -1375,14 +1384,6 @@
         }
     }
 
-    private Rect getAttachedWindowFrame() {
-        final int type = mWindowAttributes.type;
-        final boolean layoutAttached = (mParentViewRoot != null
-                && type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW
-                && type != TYPE_APPLICATION_ATTACHED_DIALOG);
-        return layoutAttached ? mParentViewRoot.mWinFrame : null;
-    }
-
     /**
      * Register any kind of listeners if setView was success.
      */
@@ -1740,16 +1741,20 @@
 
         final Rect frame = frames.frame;
         final Rect displayFrame = frames.displayFrame;
+        final Rect attachedFrame = frames.attachedFrame;
         if (mTranslator != null) {
             mTranslator.translateRectInScreenToAppWindow(frame);
             mTranslator.translateRectInScreenToAppWindow(displayFrame);
+            mTranslator.translateRectInScreenToAppWindow(attachedFrame);
         }
         final boolean frameChanged = !mWinFrame.equals(frame);
         final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
+        final boolean attachedFrameChanged = LOCAL_LAYOUT
+                && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame);
         final boolean displayChanged = mDisplay.getDisplayId() != displayId;
         final boolean resizeModeChanged = mResizeMode != resizeMode;
-        if (msg == MSG_RESIZED && !frameChanged && !configChanged && !displayChanged
-                && !resizeModeChanged && !forceNextWindowRelayout) {
+        if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged
+                && !displayChanged && !resizeModeChanged && !forceNextWindowRelayout) {
             return;
         }
 
@@ -1767,6 +1772,9 @@
 
         setFrame(frame);
         mTmpFrames.displayFrame.set(displayFrame);
+        if (mTmpFrames.attachedFrame != null && attachedFrame != null) {
+            mTmpFrames.attachedFrame.set(attachedFrame);
+        }
 
         if (mDragResizing && mUseMTRenderer) {
             boolean fullscreen = frame.equals(mPendingBackDropFrame);
@@ -2700,6 +2708,9 @@
 
         mIsInTraversal = true;
         mWillDrawSoon = true;
+        boolean cancelDraw = false;
+        boolean isSyncRequest = false;
+
         boolean windowSizeMayChange = false;
         WindowManager.LayoutParams lp = mWindowAttributes;
 
@@ -2805,10 +2816,6 @@
         // Execute enqueued actions on every traversal in case a detached view enqueued an action
         getRunQueue().executeActions(mAttachInfo.mHandler);
 
-        if (mApplyInsetsRequested) {
-            dispatchApplyInsets(host);
-        }
-
         if (mFirst) {
             // make sure touch mode code executes by setting cached value
             // to opposite of the added touch mode.
@@ -2872,6 +2879,18 @@
             }
         }
 
+        if (mApplyInsetsRequested) {
+            dispatchApplyInsets(host);
+            if (mLayoutRequested) {
+                // Short-circuit catching a new layout request here, so
+                // we don't need to go through two layout passes when things
+                // change due to fitting system windows, which can happen a lot.
+                windowSizeMayChange |= measureHierarchy(host, lp,
+                        mView.getContext().getResources(),
+                        desiredWindowWidth, desiredWindowHeight);
+            }
+        }
+
         if (layoutRequested) {
             // Clear this now, so that if anything requests a layout in the
             // rest of this function we will catch it and re-run a full
@@ -2969,6 +2988,8 @@
                     mViewFrameInfo.flags |= FrameInfo.FLAG_WINDOW_VISIBILITY_CHANGED;
                 }
                 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
+                cancelDraw = (relayoutResult & RELAYOUT_RES_CANCEL_AND_REDRAW)
+                        == RELAYOUT_RES_CANCEL_AND_REDRAW;
                 final boolean dragResizing = mPendingDragResizing;
                 if (mSyncSeqId > mLastSyncSeqId) {
                     mLastSyncSeqId = mSyncSeqId;
@@ -2977,6 +2998,7 @@
                     }
                     reportNextDraw();
                     mSyncBuffer = true;
+                    isSyncRequest = true;
                 }
 
                 final boolean surfaceControlChanged =
@@ -3265,6 +3287,19 @@
                 }
             }
         } else {
+            // If a relayout isn't going to happen, we still need to check if this window can draw
+            // when mCheckIfCanDraw is set. This is because it means we had a sync in the past, but
+            // have not been told by WMS that the sync is complete and that we can continue to draw
+            if (mCheckIfCanDraw) {
+                try {
+                    cancelDraw = mWindowSession.cancelDraw(mWindow);
+                    if (DEBUG_BLAST) {
+                        Log.d(mTag, "cancelDraw returned " + cancelDraw);
+                    }
+                } catch (RemoteException e) {
+                }
+            }
+
             // Not the first pass and no window/insets/visibility change but the window
             // may have moved and we need check that and if so to update the left and right
             // in the attach info. We translate only the window frame since on window move
@@ -3483,7 +3518,9 @@
             reportNextDraw();
         }
 
-        boolean cancelAndRedraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw();
+        mCheckIfCanDraw = isSyncRequest || cancelDraw;
+
+        boolean cancelAndRedraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || cancelDraw;
         if (!cancelAndRedraw) {
             createSyncIfNeeded();
         }
@@ -8008,69 +8045,20 @@
         final int requestedWidth = (int) (mView.getMeasuredWidth() * appScale + 0.5f);
         final int requestedHeight = (int) (mView.getMeasuredHeight() * appScale + 0.5f);
 
-        int relayoutResult = 0;
-        WindowConfiguration winConfig = getConfiguration().windowConfiguration;
-        if (LOCAL_LAYOUT) {
-            if (mFirst || viewVisibility != mViewVisibility) {
-                relayoutResult = mWindowSession.updateVisibility(mWindow, params, viewVisibility,
-                        mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);
-                if (mTranslator != null) {
-                    mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
-                    mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
-                }
-                mInsetsController.onStateChanged(mTempInsets);
-                mInsetsController.onControlsChanged(mTempControls);
-
-                mPendingAlwaysConsumeSystemBars =
-                        (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
-            }
-            final InsetsState state = mInsetsController.getState();
-            final Rect displayCutoutSafe = mTempRect;
-            state.getDisplayCutoutSafe(displayCutoutSafe);
-            if (mWindowAttributes.type == TYPE_APPLICATION_STARTING) {
-                // TODO(b/210378379): Remove the special logic.
-                // Letting starting window use the window bounds from the pending config is for the
-                // fixed rotation, because the config is not overridden before the starting window
-                // is created.
-                winConfig = mPendingMergedConfiguration.getMergedConfiguration()
-                        .windowConfiguration;
-            }
-            mWindowLayout.computeFrames(mWindowAttributes, state, displayCutoutSafe,
-                    winConfig.getBounds(), winConfig.getWindowingMode(), requestedWidth,
-                    requestedHeight, mInsetsController.getRequestedVisibilities(),
-                    getAttachedWindowFrame(), 1f /* compatScale */, mTmpFrames);
-
-            mWindowSession.updateLayout(mWindow, params,
-                    insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mTmpFrames,
-                    requestedWidth, requestedHeight);
-
-        } else {
-            relayoutResult = mWindowSession.relayout(mWindow, params,
-                    requestedWidth, requestedHeight, viewVisibility,
-                    insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
-                    mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
-                    mTempControls, mRelayoutBundle);
-            final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
-            if (maybeSyncSeqId > 0) {
-                mSyncSeqId = maybeSyncSeqId;
-            }
-
-            if (mTranslator != null) {
-                mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
-                mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame);
-                mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
-                mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
-            }
-            mInsetsController.onStateChanged(mTempInsets);
-            mInsetsController.onControlsChanged(mTempControls);
-
-            mPendingAlwaysConsumeSystemBars =
-                    (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
+        int relayoutResult = mWindowSession.relayout(mWindow, params,
+                requestedWidth, requestedHeight, viewVisibility,
+                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
+                mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
+                mTempControls, mRelayoutBundle);
+        final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid");
+        if (maybeSyncSeqId > 0) {
+            mSyncSeqId = maybeSyncSeqId;
         }
 
         final int transformHint = SurfaceControl.rotationToBufferTransform(
                 (mDisplayInstallOrientation + mDisplay.getRotation()) % 4);
 
+        final WindowConfiguration winConfig = getConfiguration().windowConfiguration;
         WindowLayout.computeSurfaceSize(mWindowAttributes, winConfig.getMaxBounds(), requestedWidth,
                 requestedHeight, mTmpFrames.frame, mPendingDragResizing, mSurfaceSize);
 
@@ -8119,10 +8107,23 @@
             destroySurface();
         }
 
+        mPendingAlwaysConsumeSystemBars =
+                (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0;
+
         if (restore) {
             params.restore();
         }
+
+        if (mTranslator != null) {
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame);
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame);
+            mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame);
+            mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets);
+            mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls);
+        }
         setFrame(mTmpFrames.frame);
+        mInsetsController.onStateChanged(mTempInsets);
+        mInsetsController.onControlsChanged(mTempControls);
         return relayoutResult;
     }
 
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index c320b26..9b6b2b9 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -66,14 +66,15 @@
     public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
             Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
             int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities,
-            Rect attachedWindowFrame, float compatScale, ClientWindowFrames outFrames) {
+            float compatScale, ClientWindowFrames frames) {
         final int type = attrs.type;
         final int fl = attrs.flags;
         final int pfl = attrs.privateFlags;
         final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) == FLAG_LAYOUT_IN_SCREEN;
-        final Rect outDisplayFrame = outFrames.displayFrame;
-        final Rect outParentFrame = outFrames.parentFrame;
-        final Rect outFrame = outFrames.frame;
+        final Rect attachedWindowFrame = frames.attachedFrame;
+        final Rect outDisplayFrame = frames.displayFrame;
+        final Rect outParentFrame = frames.parentFrame;
+        final Rect outFrame = frames.frame;
 
         // Compute bounds restricted by insets
         final Insets insets = state.calculateInsets(windowBounds, attrs.getFitInsetsTypes(),
@@ -104,7 +105,7 @@
         final DisplayCutout cutout = state.getDisplayCutout();
         final Rect displayCutoutSafeExceptMaybeBars = mTempDisplayCutoutSafeExceptMaybeBarsRect;
         displayCutoutSafeExceptMaybeBars.set(displayCutoutSafe);
-        outFrames.isParentFrameClippedByDisplayCutout = false;
+        frames.isParentFrameClippedByDisplayCutout = false;
         if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS && !cutout.isEmpty()) {
             // Ensure that windows with a non-ALWAYS display cutout mode are laid out in
             // the cutout safe zone.
@@ -167,7 +168,7 @@
             if (!attachedInParent && !floatingInScreenWindow) {
                 mTempRect.set(outParentFrame);
                 outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
-                outFrames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
+                frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
             }
             outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
         }
@@ -287,12 +288,9 @@
             }
         }
 
-        if (DEBUG) Log.d(TAG, "computeWindowFrames " + attrs.getTitle()
-                + " outFrames=" + outFrames
+        if (DEBUG) Log.d(TAG, "computeFrames " + attrs.getTitle()
+                + " frames=" + frames
                 + " windowBounds=" + windowBounds.toShortString()
-                + " attachedWindowFrame=" + (attachedWindowFrame != null
-                        ? attachedWindowFrame.toShortString()
-                        : "null")
                 + " requestedWidth=" + requestedWidth
                 + " requestedHeight=" + requestedHeight
                 + " compatScale=" + compatScale
diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java
index 85a5762..25445ab 100644
--- a/core/java/android/view/WindowManagerGlobal.java
+++ b/core/java/android/view/WindowManagerGlobal.java
@@ -83,6 +83,11 @@
     public static final int RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS = 1 << 3;
 
     /**
+     * The window manager has told the window it cannot draw this frame and should retry again.
+     */
+    public static final int RELAYOUT_RES_CANCEL_AND_REDRAW = 1 << 4;
+
+    /**
      * Flag for relayout: the client will be later giving
      * internal insets; as a result, the window will not impact other window
      * layouts until the insets are given.
diff --git a/core/java/android/view/WindowlessWindowLayout.java b/core/java/android/view/WindowlessWindowLayout.java
index 7cc50c5..5bec5b6 100644
--- a/core/java/android/view/WindowlessWindowLayout.java
+++ b/core/java/android/view/WindowlessWindowLayout.java
@@ -30,10 +30,10 @@
     public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
             Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
             int requestedWidth, int requestedHeight, InsetsVisibilities requestedVisibilities,
-            Rect attachedWindowFrame, float compatScale, ClientWindowFrames outFrames) {
-        outFrames.frame.set(0, 0, attrs.width, attrs.height);
-        outFrames.displayFrame.set(outFrames.frame);
-        outFrames.parentFrame.set(outFrames.frame);
+            float compatScale, ClientWindowFrames frames) {
+        frames.frame.set(0, 0, attrs.width, attrs.height);
+        frames.displayFrame.set(frames.frame);
+        frames.parentFrame.set(frames.frame);
     }
 }
 
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index a212348..94da274 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -149,7 +149,7 @@
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
             int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
+            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) {
         final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession)
                 .setFormat(attrs.format)
                 .setBLASTLayer()
@@ -181,6 +181,7 @@
         synchronized (this) {
             mStateForWindow.put(window.asBinder(), state);
         }
+        outAttachedFrame.set(0, 0, -1, -1);
 
         final int res = WindowManagerGlobal.ADD_OKAY | WindowManagerGlobal.ADD_FLAG_APP_VISIBLE |
                         WindowManagerGlobal.ADD_FLAG_USE_BLAST;
@@ -196,15 +197,15 @@
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
             int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
+            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) {
         return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities,
-                outInputChannel, outInsetsState, outActiveControls);
+                outInputChannel, outInsetsState, outActiveControls, outAttachedFrame);
     }
 
     @Override
     public int addToDisplayWithoutInputChannel(android.view.IWindow window,
             android.view.WindowManager.LayoutParams attrs, int viewVisibility, int layerStackId,
-            android.view.InsetsState insetsState) {
+            android.view.InsetsState insetsState, Rect outAttachedFrame) {
         return 0;
     }
 
@@ -337,21 +338,6 @@
     }
 
     @Override
-    public int updateVisibility(IWindow window, WindowManager.LayoutParams inAttrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        // TODO(b/161810301): Finish the implementation.
-        return 0;
-    }
-
-    @Override
-    public void updateLayout(IWindow window, WindowManager.LayoutParams inAttrs, int flags,
-            ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) {
-        // TODO(b/161810301): Finish the implementation.
-    }
-
-    @Override
     public void prepareToReplaceWindows(android.os.IBinder appToken, boolean childrenOnly) {
     }
 
@@ -552,4 +538,9 @@
             }
         }
     }
+
+    @Override
+    public boolean cancelDraw(IWindow window) {
+        return false;
+    }
 }
diff --git a/core/java/android/window/ClientWindowFrames.java b/core/java/android/window/ClientWindowFrames.java
index 51f3fe2..929e81ed 100644
--- a/core/java/android/window/ClientWindowFrames.java
+++ b/core/java/android/window/ClientWindowFrames.java
@@ -17,6 +17,7 @@
 package android.window;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.graphics.Rect;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -40,6 +41,12 @@
      */
     public final @NonNull Rect parentFrame = new Rect();
 
+    /**
+     * The frame this window attaches to. If this is not null, this is the frame of the parent
+     * window.
+     */
+    public @Nullable Rect attachedFrame;
+
     public boolean isParentFrameClippedByDisplayCutout;
 
     public ClientWindowFrames() {
@@ -49,6 +56,9 @@
         frame.set(other.frame);
         displayFrame.set(other.displayFrame);
         parentFrame.set(other.parentFrame);
+        if (other.attachedFrame != null) {
+            attachedFrame = new Rect(other.attachedFrame);
+        }
         isParentFrameClippedByDisplayCutout = other.isParentFrameClippedByDisplayCutout;
     }
 
@@ -61,6 +71,7 @@
         frame.readFromParcel(in);
         displayFrame.readFromParcel(in);
         parentFrame.readFromParcel(in);
+        attachedFrame = in.readTypedObject(Rect.CREATOR);
         isParentFrameClippedByDisplayCutout = in.readBoolean();
     }
 
@@ -69,6 +80,7 @@
         frame.writeToParcel(dest, flags);
         displayFrame.writeToParcel(dest, flags);
         parentFrame.writeToParcel(dest, flags);
+        dest.writeTypedObject(attachedFrame, flags);
         dest.writeBoolean(isParentFrameClippedByDisplayCutout);
     }
 
@@ -78,6 +90,7 @@
         return "ClientWindowFrames{frame=" + frame.toShortString(sb)
                 + " display=" + displayFrame.toShortString(sb)
                 + " parentFrame=" + parentFrame.toShortString(sb)
+                + (attachedFrame != null ? " attachedFrame=" + attachedFrame.toShortString() : "")
                 + " parentClippedByDisplayCutout=" + isParentFrameClippedByDisplayCutout + "}";
     }
 
diff --git a/core/java/android/window/SizeConfigurationBuckets.java b/core/java/android/window/SizeConfigurationBuckets.java
index f474f0a..998bec0 100644
--- a/core/java/android/window/SizeConfigurationBuckets.java
+++ b/core/java/android/window/SizeConfigurationBuckets.java
@@ -104,24 +104,15 @@
     /**
      * Get the changes between two configurations but don't count changes in sizes if they don't
      * cross boundaries that are important to the app.
-     *
-     * This is a static helper to deal with null `buckets`. When no buckets have been specified,
-     * this actually filters out all 3 size-configs. This is legacy behavior.
      */
     public static int filterDiff(int diff, @NonNull Configuration oldConfig,
             @NonNull Configuration newConfig, @Nullable SizeConfigurationBuckets buckets) {
+        if (buckets == null) {
+            return diff;
+        }
+
         final boolean nonSizeLayoutFieldsUnchanged =
                 areNonSizeLayoutFieldsUnchanged(oldConfig.screenLayout, newConfig.screenLayout);
-        if (buckets == null) {
-            // Only unflip CONFIG_SCREEN_LAYOUT if non-size-related  attributes of screen layout do
-            // not change.
-            if (nonSizeLayoutFieldsUnchanged) {
-                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE
-                        | CONFIG_SCREEN_LAYOUT);
-            } else {
-                return diff & ~(CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE);
-            }
-        }
         if ((diff & CONFIG_SCREEN_SIZE) != 0) {
             final boolean crosses = buckets.crossesHorizontalSizeThreshold(oldConfig.screenWidthDp,
                     newConfig.screenWidthDp)
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 52fd7fe..22340c6 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -75,6 +75,8 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TAKE_SCREENSHOT;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__UNFOLD_ANIM;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_SWITCH;
@@ -206,6 +208,8 @@
     public static final int CUJ_SETTINGS_TOGGLE = 57;
     public static final int CUJ_SHADE_DIALOG_OPEN = 58;
     public static final int CUJ_USER_DIALOG_OPEN = 59;
+    public static final int CUJ_TASKBAR_EXPAND = 60;
+    public static final int CUJ_TASKBAR_COLLAPSE = 61;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -274,6 +278,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SETTINGS_TOGGLE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_DIALOG_OPEN,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__USER_DIALOG_OPEN,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -354,6 +360,8 @@
             CUJ_SETTINGS_TOGGLE,
             CUJ_SHADE_DIALOG_OPEN,
             CUJ_USER_DIALOG_OPEN,
+            CUJ_TASKBAR_EXPAND,
+            CUJ_TASKBAR_COLLAPSE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -792,6 +800,10 @@
                 return "SHADE_DIALOG_OPEN";
             case CUJ_USER_DIALOG_OPEN:
                 return "USER_DIALOG_OPEN";
+            case CUJ_TASKBAR_EXPAND:
+                return "TASKBAR_EXPAND";
+            case CUJ_TASKBAR_COLLAPSE:
+                return "TASKBAR_COLLAPSE";
         }
         return "UNKNOWN";
     }
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index 0756d68..fd787f6 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -138,7 +138,7 @@
 
         </LinearLayout>
 
-        <ImageView
+        <com.android.internal.widget.CachingIconView
             android:id="@+id/right_icon"
             android:layout_width="@dimen/notification_right_icon_size"
             android:layout_height="@dimen/notification_right_icon_size"
@@ -150,6 +150,8 @@
             android:clipToOutline="true"
             android:importantForAccessibility="no"
             android:scaleType="centerCrop"
+            android:maxDrawableWidth="@dimen/notification_right_icon_size"
+            android:maxDrawableHeight="@dimen/notification_right_icon_size"
             />
 
         <FrameLayout
diff --git a/core/res/res/layout/notification_template_right_icon.xml b/core/res/res/layout/notification_template_right_icon.xml
index f163ed5..8b3b795 100644
--- a/core/res/res/layout/notification_template_right_icon.xml
+++ b/core/res/res/layout/notification_template_right_icon.xml
@@ -13,7 +13,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-<ImageView
+<com.android.internal.widget.CachingIconView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/right_icon"
     android:layout_width="@dimen/notification_right_icon_size"
@@ -25,4 +25,6 @@
     android:clipToOutline="true"
     android:importantForAccessibility="no"
     android:scaleType="centerCrop"
+    android:maxDrawableWidth="@dimen/notification_right_icon_size"
+    android:maxDrawableHeight="@dimen/notification_right_icon_size"
     />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2171987..a8c7bf2 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2944,7 +2944,7 @@
 
     <!-- System bluetooth stack package name -->
     <string name="config_systemBluetoothStack" translatable="false">
-        com.android.bluetooth.services
+        com.android.bluetooth
     </string>
 
     <!-- Flag indicating that the media framework should not allow changes or mute on any
@@ -5180,11 +5180,11 @@
     <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. -->
     <bool name="config_letterboxIsEducationEnabled">false</bool>
 
-    <!-- Default min aspect ratio for unresizable apps which is used when an app doesn't specify
-         android:minAspectRatio in accordance with CDD 7.1.1.2 requirement:
-         https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio.
-         An exception will be thrown if the given aspect ratio < 4:3.  -->
-    <item name="config_letterboxDefaultMinAspectRatioForUnresizableApps" format="float" type="dimen">1.5</item>
+    <!-- Default min aspect ratio for unresizable apps which are eligible for size compat mode.
+         Values <= 1.0 will be ignored. Activity min/max aspect ratio restrictions will still be
+         espected so this override can control the maximum screen area that can be occupied by
+         the app in the letterbox mode. -->
+    <item name="config_letterboxDefaultMinAspectRatioForUnresizableApps" format="float" type="dimen">0.0</item>
 
     <!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. -->
     <bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool>
diff --git a/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java
new file mode 100644
index 0000000..7c76498
--- /dev/null
+++ b/core/tests/mockingcoretests/src/android/os/BundleRecyclingTest.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed.
+ *
+ * <p>Build/Install/Run:
+ *  atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Presubmit
+public class BundleRecyclingTest {
+    private Parcel mParcelSpy;
+    private Bundle mBundle;
+
+    @Before
+    public void setUp() throws Exception {
+        setUpBundle(/* hasLazy */ true);
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() {
+        setUpBundle(/* hasLazy */ false);
+        // Will unparcel and immediately recycle parcel
+        assertNotNull(mBundle.getString("key"));
+        verify(mParcelSpy, times(1)).recycle();
+        assertFalse(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+    }
+
+    @Test
+    public void bundleClear_whenParcelled_recyclesParcel() {
+        assertTrue(mBundle.isParcelled());
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
+        // Will unparcel with a lazy value, and immediately unwrap the lazy value,
+        // with no lazy values left at the end of getParcelable
+        assertNotNull(mBundle.getParcelable("key", CustomParcelable.class));
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() {
+        // Will unparcel but keep the CustomParcelable lazy
+        assertFalse(mBundle.isEmpty());
+        verify(mParcelSpy, times(0)).recycle();
+
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        // Should not recycle again
+        mBundle.clear();
+        verify(mParcelSpy, times(1)).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() {
+        Bundle copy = new Bundle();
+        copy.putAll(mBundle);
+
+        mBundle.clear();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        copy.clear();
+        assertTrue(copy.isDefinitelyEmpty());
+
+        verify(mParcelSpy, never()).recycle();
+    }
+
+    @Test
+    public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() {
+        // Will unparcel but keep the CustomParcelable lazy
+        assertFalse(mBundle.isEmpty());
+
+        Bundle copy = mBundle.deepCopy();
+        copy.putAll(mBundle);
+
+        mBundle.clear();
+        assertTrue(mBundle.isDefinitelyEmpty());
+
+        copy.clear();
+        assertTrue(copy.isDefinitelyEmpty());
+
+        verify(mParcelSpy, never()).recycle();
+    }
+
+    private void setUpBundle(boolean hasLazy) {
+        AtomicReference<Parcel> parcel = new AtomicReference<>();
+        StaticMockitoSession session = mockitoSession()
+                .strictness(Strictness.STRICT_STUBS)
+                .spyStatic(Parcel.class)
+                .startMocking();
+        doAnswer((Answer<Parcel>) invocationOnSpy -> {
+            Parcel spy = (Parcel) invocationOnSpy.callRealMethod();
+            spyOn(spy);
+            parcel.set(spy);
+            return spy;
+        }).when(() -> Parcel.obtain());
+
+        Bundle bundle = new Bundle();
+        bundle.setClassLoader(getClass().getClassLoader());
+        Parcel p = createBundle(hasLazy);
+        bundle.readFromParcel(p);
+        p.recycle();
+
+        session.finishMocking();
+
+        mParcelSpy = parcel.get();
+        mBundle = bundle;
+    }
+
+    /**
+     * Create a test bundle, parcel it and return the parcel.
+     */
+    private Parcel createBundle(boolean hasLazy) {
+        final Bundle source = new Bundle();
+        if (hasLazy) {
+            source.putParcelable("key", new CustomParcelable(13, "Tiramisu"));
+        } else {
+            source.putString("key", "tiramisu");
+        }
+        return getParcelledBundle(source);
+    }
+
+    /**
+     * Take a bundle, write it to a parcel and return the parcel.
+     */
+    private Parcel getParcelledBundle(Bundle bundle) {
+        final Parcel p = Parcel.obtain();
+        // Don't use p.writeParcelabe(), which would write the creator, which we don't need.
+        bundle.writeToParcel(p, 0);
+        p.setDataPosition(0);
+        return p;
+    }
+
+    private static class CustomParcelable implements Parcelable {
+        public final int integer;
+        public final String string;
+
+        CustomParcelable(int integer, String string) {
+            this.integer = integer;
+            this.string = string;
+        }
+
+        protected CustomParcelable(Parcel in) {
+            integer = in.readInt();
+            string = in.readString();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(integer);
+            out.writeString(string);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof CustomParcelable)) {
+                return false;
+            }
+            CustomParcelable that = (CustomParcelable) other;
+            return integer == that.integer && string.equals(that.string);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(integer, string);
+        }
+
+        public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() {
+            @Override
+            public CustomParcelable createFromParcel(Parcel in) {
+                return new CustomParcelable(in);
+            }
+            @Override
+            public CustomParcelable[] newArray(int size) {
+                return new CustomParcelable[size];
+            }
+        };
+    }
+}
diff --git a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
index fa4aa80..ed857e8 100644
--- a/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
+++ b/core/tests/mockingcoretests/src/android/window/SizeConfigurationBucketsTest.java
@@ -88,26 +88,15 @@
     }
 
     /**
-     * Tests that null size configuration buckets unflips the correct configuration flags.
+     * Tests that {@code null} size configuration buckets do not unflip the configuration flags.
      */
     @Test
     public void testNullSizeConfigurationBuckets() {
-        // Check that all 3 size configurations are filtered out of the diff if the buckets are null
-        // and non-size attributes of screen layout are unchanged. Add a non-size related config
-        // change (i.e. CONFIG_LOCALE) to test that the diff is not set to zero.
         final int diff = CONFIG_SCREEN_SIZE | CONFIG_SMALLEST_SCREEN_SIZE | CONFIG_SCREEN_LAYOUT
                 | CONFIG_LOCALE;
         final int filteredDiffNonSizeLayoutUnchanged = SizeConfigurationBuckets.filterDiff(diff,
                 Configuration.EMPTY, Configuration.EMPTY, null);
-        assertEquals(CONFIG_LOCALE, filteredDiffNonSizeLayoutUnchanged);
-
-        // Check that only screen size and smallest screen size are filtered out of the diff if the
-        // buckets are null and non-size attributes of screen layout are changed.
-        final Configuration newConfig = new Configuration();
-        newConfig.screenLayout |= SCREENLAYOUT_ROUND_YES;
-        final int filteredDiffNonSizeLayoutChanged = SizeConfigurationBuckets.filterDiff(diff,
-                Configuration.EMPTY, newConfig, null);
-        assertEquals(CONFIG_SCREEN_LAYOUT | CONFIG_LOCALE, filteredDiffNonSizeLayoutChanged);
+        assertEquals(diff, filteredDiffNonSizeLayoutUnchanged);
     }
 
     /**
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index f8da95d..6706e4e 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -475,12 +475,6 @@
       "group": "WM_DEBUG_ADD_REMOVE",
       "at": "com\/android\/server\/wm\/ResetTargetTaskHelper.java"
     },
-    "-1635750891": {
-      "message": "Received remote change for Display[%d], applied: [%dx%d, rot = %d]",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_CONFIGURATION",
-      "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java"
-    },
     "-1633115609": {
       "message": "Key dispatch not paused for screen off",
       "level": "VERBOSE",
@@ -1711,6 +1705,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-417730399": {
+      "message": "Preparing to sync a window that was already in the sync, so try dropping buffer. win=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_SYNC_ENGINE",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "-415865166": {
       "message": "findFocusedWindow: Found new focus @ %s",
       "level": "VERBOSE",
@@ -2137,6 +2137,12 @@
       "group": "WM_DEBUG_RECENTS_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RecentsAnimation.java"
     },
+    "-4263657": {
+      "message": "Got a buffer for request id=%d but latest request is id=%d. Since the buffer is out-of-date, drop it. win=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_SYNC_ENGINE",
+      "at": "com\/android\/server\/wm\/WindowState.java"
+    },
     "3593205": {
       "message": "commitVisibility: %s: visible=%b mVisibleRequested=%b",
       "level": "VERBOSE",
@@ -2599,6 +2605,12 @@
       "group": "WM_DEBUG_STATES",
       "at": "com\/android\/server\/wm\/TaskFragment.java"
     },
+    "385237117": {
+      "message": "moveFocusableActivityToTop: already on top and focused, activity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "385595355": {
       "message": "Starting animation on %s: type=%d, anim=%s",
       "level": "VERBOSE",
@@ -3403,6 +3415,12 @@
       "group": "WM_DEBUG_BOOT",
       "at": "com\/android\/server\/wm\/WindowManagerService.java"
     },
+    "1239439010": {
+      "message": "moveFocusableActivityToTop: set focused, activity=%s",
+      "level": "DEBUG",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/ActivityRecord.java"
+    },
     "1252594551": {
       "message": "Window types in WindowContext and LayoutParams.type should match! Type from LayoutParams is %d, but type from WindowContext is %d",
       "level": "WARN",
@@ -3877,12 +3895,6 @@
       "group": "WM_DEBUG_ORIENTATION",
       "at": "com\/android\/server\/wm\/WindowStateAnimator.java"
     },
-    "1764619787": {
-      "message": "Remote change for Display[%d]: timeout reached",
-      "level": "VERBOSE",
-      "group": "WM_DEBUG_CONFIGURATION",
-      "at": "com\/android\/server\/wm\/RemoteDisplayChangeController.java"
-    },
     "1774661765": {
       "message": "Devices still not ready after waiting %d milliseconds before attempting to detect safe mode.",
       "level": "WARN",
@@ -3991,12 +4003,6 @@
       "group": "WM_DEBUG_STARTING_WINDOW",
       "at": "com\/android\/server\/wm\/ActivityRecord.java"
     },
-    "1856211951": {
-      "message": "moveFocusableActivityToTop: already on top, activity=%s",
-      "level": "DEBUG",
-      "group": "WM_DEBUG_FOCUS",
-      "at": "com\/android\/server\/wm\/ActivityRecord.java"
-    },
     "1856783490": {
       "message": "resumeTopActivity: Restarting %s",
       "level": "DEBUG",
diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java
index 1629b6a..239621e 100644
--- a/graphics/java/android/graphics/ImageDecoder.java
+++ b/graphics/java/android/graphics/ImageDecoder.java
@@ -40,6 +40,7 @@
 import android.graphics.drawable.NinePatchDrawable;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Trace;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.DisplayMetrics;
@@ -223,13 +224,21 @@
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
             return nCreate(mData, mOffset, mLength, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "ByteArraySource{len=" + mLength + "}";
+        }
     }
 
     private static class ByteBufferSource extends Source {
         ByteBufferSource(@NonNull ByteBuffer buffer) {
             mBuffer = buffer;
+            mLength = mBuffer.limit() - mBuffer.position();
         }
+
         private final ByteBuffer mBuffer;
+        private final int mLength;
 
         @Override
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
@@ -241,6 +250,11 @@
             ByteBuffer buffer = mBuffer.slice();
             return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "ByteBufferSource{len=" + mLength + "}";
+        }
     }
 
     private static class ContentResolverSource extends Source {
@@ -285,6 +299,16 @@
 
             return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            String uri = mUri.toString();
+            if (uri.length() > 90) {
+                // We want to keep the Uri usable - usually the authority and the end is important.
+                uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10);
+            }
+            return "ContentResolverSource{uri=" + uri + "}";
+        }
     }
 
     @NonNull
@@ -399,6 +423,11 @@
                 return createFromStream(is, false, preferAnimation, this);
             }
         }
+
+        @Override
+        public String toString() {
+            return "InputStream{s=" + mInputStream + "}";
+        }
     }
 
     /**
@@ -444,6 +473,11 @@
                 return createFromAsset(ais, preferAnimation, this);
             }
         }
+
+        @Override
+        public String toString() {
+            return "AssetInputStream{s=" + mAssetInputStream + "}";
+        }
     }
 
     private static class ResourceSource extends Source {
@@ -485,6 +519,17 @@
 
             return createFromAsset((AssetInputStream) is, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            // Try to return a human-readable name for debugging purposes.
+            try {
+                return "Resource{name=" + mResources.getResourceName(mResId) + "}";
+            } catch (Resources.NotFoundException e) {
+                // It's ok if we don't find it, fall back to ID.
+            }
+            return "Resource{id=" + mResId + "}";
+        }
     }
 
     /**
@@ -521,6 +566,11 @@
             InputStream is = mAssets.open(mFileName);
             return createFromAsset((AssetInputStream) is, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "AssetSource{file=" + mFileName + "}";
+        }
     }
 
     private static class FileSource extends Source {
@@ -534,6 +584,11 @@
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
             return createFromFile(mFile, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "FileSource{file=" + mFile + "}";
+        }
     }
 
     private static class CallableSource extends Source {
@@ -557,6 +612,11 @@
             }
             return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "CallableSource{obj=" + mCallable.toString() + "}";
+        }
     }
 
     /**
@@ -1763,61 +1823,65 @@
     @NonNull
     private static Drawable decodeDrawableImpl(@NonNull Source src,
             @Nullable OnHeaderDecodedListener listener) throws IOException {
+        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable");
         try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) {
             decoder.mSource = src;
             decoder.callHeaderDecoded(listener, src);
 
-            if (decoder.mUnpremultipliedRequired) {
-                // Though this could be supported (ignored) for opaque images,
-                // it seems better to always report this error.
-                throw new IllegalStateException("Cannot decode a Drawable " +
-                                                "with unpremultiplied pixels!");
-            }
-
-            if (decoder.mMutable) {
-                throw new IllegalStateException("Cannot decode a mutable " +
-                                                "Drawable!");
-            }
-
-            // this call potentially manipulates the decoder so it must be performed prior to
-            // decoding the bitmap and after decode set the density on the resulting bitmap
-            final int srcDensity = decoder.computeDensity(src);
-            if (decoder.mAnimated) {
-                // AnimatedImageDrawable calls postProcessAndRelease only if
-                // mPostProcessor exists.
-                ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
-                        null : decoder;
-                decoder.checkState(true);
-                Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
-                        postProcessPtr, decoder.mDesiredWidth,
-                        decoder.mDesiredHeight, decoder.getColorSpacePtr(),
-                        decoder.checkForExtended(), srcDensity,
-                        src.computeDstDensity(), decoder.mCropRect,
-                        decoder.mInputStream, decoder.mAssetFd);
-                // d has taken ownership of these objects.
-                decoder.mInputStream = null;
-                decoder.mAssetFd = null;
-                return d;
-            }
-
-            Bitmap bm = decoder.decodeBitmapInternal();
-            bm.setDensity(srcDensity);
-
-            Resources res = src.getResources();
-            byte[] np = bm.getNinePatchChunk();
-            if (np != null && NinePatch.isNinePatchChunk(np)) {
-                Rect opticalInsets = new Rect();
-                bm.getOpticalInsets(opticalInsets);
-                Rect padding = decoder.mOutPaddingRect;
-                if (padding == null) {
-                    padding = new Rect();
+            try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
+                if (decoder.mUnpremultipliedRequired) {
+                    // Though this could be supported (ignored) for opaque images,
+                    // it seems better to always report this error.
+                    throw new IllegalStateException(
+                            "Cannot decode a Drawable with unpremultiplied pixels!");
                 }
-                nGetPadding(decoder.mNativePtr, padding);
-                return new NinePatchDrawable(res, bm, np, padding,
-                        opticalInsets, null);
-            }
 
-            return new BitmapDrawable(res, bm);
+                if (decoder.mMutable) {
+                    throw new IllegalStateException("Cannot decode a mutable Drawable!");
+                }
+
+                // this call potentially manipulates the decoder so it must be performed prior to
+                // decoding the bitmap and after decode set the density on the resulting bitmap
+                final int srcDensity = decoder.computeDensity(src);
+                if (decoder.mAnimated) {
+                    // AnimatedImageDrawable calls postProcessAndRelease only if
+                    // mPostProcessor exists.
+                    ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? null : decoder;
+                    decoder.checkState(true);
+                    Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+                            postProcessPtr, decoder.mDesiredWidth,
+                            decoder.mDesiredHeight, decoder.getColorSpacePtr(),
+                            decoder.checkForExtended(), srcDensity,
+                            src.computeDstDensity(), decoder.mCropRect,
+                            decoder.mInputStream, decoder.mAssetFd);
+                    // d has taken ownership of these objects.
+                    decoder.mInputStream = null;
+                    decoder.mAssetFd = null;
+                    return d;
+                }
+
+                Bitmap bm = decoder.decodeBitmapInternal();
+                bm.setDensity(srcDensity);
+
+                Resources res = src.getResources();
+                byte[] np = bm.getNinePatchChunk();
+                if (np != null && NinePatch.isNinePatchChunk(np)) {
+                    Rect opticalInsets = new Rect();
+                    bm.getOpticalInsets(opticalInsets);
+                    Rect padding = decoder.mOutPaddingRect;
+                    if (padding == null) {
+                        padding = new Rect();
+                    }
+                    nGetPadding(decoder.mNativePtr, padding);
+                    return new NinePatchDrawable(res, bm, np, padding,
+                            opticalInsets, null);
+                }
+
+                return new BitmapDrawable(res, bm);
+            }
+        } finally {
+            // Close the ImageDecoder#decode trace.
+            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
         }
     }
 
@@ -1867,26 +1931,51 @@
     @NonNull
     private static Bitmap decodeBitmapImpl(@NonNull Source src,
             @Nullable OnHeaderDecodedListener listener) throws IOException {
+        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap");
         try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) {
             decoder.mSource = src;
             decoder.callHeaderDecoded(listener, src);
+            try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
+                // this call potentially manipulates the decoder so it must be performed prior to
+                // decoding the bitmap
+                final int srcDensity = decoder.computeDensity(src);
+                Bitmap bm = decoder.decodeBitmapInternal();
+                bm.setDensity(srcDensity);
 
-            // this call potentially manipulates the decoder so it must be performed prior to
-            // decoding the bitmap
-            final int srcDensity = decoder.computeDensity(src);
-            Bitmap bm = decoder.decodeBitmapInternal();
-            bm.setDensity(srcDensity);
+                Rect padding = decoder.mOutPaddingRect;
+                if (padding != null) {
+                    byte[] np = bm.getNinePatchChunk();
+                    if (np != null && NinePatch.isNinePatchChunk(np)) {
+                        nGetPadding(decoder.mNativePtr, padding);
+                    }
+                }
+                return bm;
+            }
+        } finally {
+            // Close the ImageDecoder#decode trace.
+            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+        }
+    }
 
-            Rect padding = decoder.mOutPaddingRect;
-            if (padding != null) {
-                byte[] np = bm.getNinePatchChunk();
-                if (np != null && NinePatch.isNinePatchChunk(np)) {
-                    nGetPadding(decoder.mNativePtr, padding);
+    /**
+     * This describes the decoder in traces to ease debugging. It has to be called after
+     * header has been decoded and width/height have been populated. It should be used
+     * inside a try-with-resources call to automatically complete the trace.
+     */
+    private static AutoCloseable traceDecoderSource(ImageDecoder decoder) {
+        final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
+        if (resourceTracingEnabled) {
+            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
+        }
+
+        return new AutoCloseable() {
+            @Override
+            public void close() throws Exception {
+                if (resourceTracingEnabled) {
+                    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
                 }
             }
-
-            return bm;
-        }
+        };
     }
 
     // This method may modify the decoder so it must be called prior to performing the decode
@@ -1994,6 +2083,66 @@
         }
     }
 
+    /**
+     * Returns a short string describing what passed ImageDecoder is loading -
+     * it reports image dimensions, desired dimensions (if any) and source resource.
+     *
+     * The string appears in perf traces to simplify search for slow or memory intensive
+     * image loads.
+     *
+     * Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource}
+     *
+     * @hide
+     */
+    private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) {
+        StringBuilder builder = new StringBuilder();
+        // Source dimensions
+        builder.append("ID#w=");
+        builder.append(decoder.mWidth);
+        builder.append(";h=");
+        builder.append(decoder.mHeight);
+        // Desired dimensions (if present)
+        if (decoder.mDesiredWidth != decoder.mWidth
+                || decoder.mDesiredHeight != decoder.mHeight) {
+            builder.append(";dw=");
+            builder.append(decoder.mDesiredWidth);
+            builder.append(";dh=");
+            builder.append(decoder.mDesiredHeight);
+        }
+        // Source description
+        builder.append(";src=");
+        builder.append(decoder.mSource);
+        return builder.toString();
+    }
+
+    /**
+     * Records a trace with information about the source being decoded - dimensions,
+     * desired dimensions and source information.
+     *
+     * It significantly eases debugging of slow resource loads on main thread and
+     * possible large memory consumers.
+     *
+     * @hide
+     */
+    private static final class ImageDecoderSourceTrace implements AutoCloseable {
+
+        private final boolean mResourceTracingEnabled;
+
+        ImageDecoderSourceTrace(ImageDecoder decoder) {
+            mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
+            if (mResourceTracingEnabled) {
+                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
+            }
+        }
+
+        @Override
+        public void close() {
+            if (mResourceTracingEnabled) {
+                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+            }
+        }
+    }
+
     private static native ImageDecoder nCreate(long asset,
             boolean preferAnimation, Source src) throws IOException;
     private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 242e9ab..41791af 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -24,9 +24,9 @@
 import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
-import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
 import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
-import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
+import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds;
 import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
 
 import android.app.Activity;
@@ -381,6 +381,7 @@
      *         in a state that the caller shouldn't handle.
      */
     @VisibleForTesting
+    @GuardedBy("mLock")
     boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) {
         if (isInPictureInPicture(activity) || activity.isFinishing()) {
             // We don't embed activity when it is in PIP, or finishing. Return true since we don't
@@ -581,8 +582,9 @@
     }
 
     /** Finds the activity below the given activity. */
+    @VisibleForTesting
     @Nullable
-    private Activity findActivityBelow(@NonNull Activity activity) {
+    Activity findActivityBelow(@NonNull Activity activity) {
         Activity activityBelow = null;
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         if (container != null) {
@@ -606,6 +608,7 @@
      * Checks if there is a rule to split the two activities. If there is one, puts them into split
      * and returns {@code true}. Otherwise, returns {@code false}.
      */
+    @GuardedBy("mLock")
     private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
         final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
@@ -616,25 +619,25 @@
                 primaryActivity);
         final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
         if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
-                && canReuseContainer(splitRule, splitContainer.getSplitRule())
-                && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(),
-                        getMinDimensions(primaryActivity))) {
+                && canReuseContainer(splitRule, splitContainer.getSplitRule())) {
             // Can launch in the existing secondary container if the rules share the same
             // presentation.
             final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
-            if (secondaryContainer == getContainerWithActivity(secondaryActivity)
-                    && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(),
-                            getMinDimensions(secondaryActivity))) {
+            if (secondaryContainer == getContainerWithActivity(secondaryActivity)) {
                 // The activity is already in the target TaskFragment.
                 return true;
             }
             secondaryContainer.addPendingAppearedActivity(secondaryActivity);
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            wct.reparentActivityToTaskFragment(
-                    secondaryContainer.getTaskFragmentToken(),
-                    secondaryActivity.getActivityToken());
-            mPresenter.applyTransaction(wct);
-            return true;
+            if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
+                    secondaryActivity, null /* secondaryIntent */)
+                    != RESULT_EXPAND_FAILED_NO_TF_INFO) {
+                wct.reparentActivityToTaskFragment(
+                        secondaryContainer.getTaskFragmentToken(),
+                        secondaryActivity.getActivityToken());
+                mPresenter.applyTransaction(wct);
+                return true;
+            }
         }
         // Create new split pair.
         mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
@@ -642,6 +645,11 @@
     }
 
     private void onActivityConfigurationChanged(@NonNull Activity activity) {
+        if (activity.isFinishing()) {
+            // Do nothing if the activity is currently finishing.
+            return;
+        }
+
         if (isInPictureInPicture(activity)) {
             // We don't embed activity when it is in PIP.
             return;
@@ -787,6 +795,7 @@
      * Returns a container for the new activity intent to launch into as splitting with the primary
      * activity.
      */
+    @GuardedBy("mLock")
     @Nullable
     private TaskFragmentContainer getSecondaryContainerForSplitIfAny(
             @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
@@ -800,16 +809,12 @@
         if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer()
                 && (canReuseContainer(splitRule, splitContainer.getSplitRule())
                 // TODO(b/231845476) we should always respect clearTop.
-                || !respectClearTop)) {
-            final Rect secondaryBounds = splitContainer.getSecondaryContainer()
-                    .getLastRequestedBounds();
-            if (secondaryBounds.isEmpty()
-                    || !boundsSmallerThanMinDimensions(secondaryBounds,
-                            getMinDimensions(intent))) {
-                // Can launch in the existing secondary container if the rules share the same
-                // presentation.
-                return splitContainer.getSecondaryContainer();
-            }
+                || !respectClearTop)
+                && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
+                        null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) {
+            // Can launch in the existing secondary container if the rules share the same
+            // presentation.
+            return splitContainer.getSecondaryContainer();
         }
         // Create a new TaskFragment to split with the primary activity for the new activity.
         return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent,
@@ -863,6 +868,7 @@
      *                                  if needed.
      * @param taskId                    parent Task of the new TaskFragment.
      */
+    @GuardedBy("mLock")
     TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
             @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) {
         if (activityInTask == null) {
@@ -876,7 +882,7 @@
                 pendingAppearedIntent, taskContainer, this);
         if (!taskContainer.isTaskBoundsInitialized()) {
             // Get the initial bounds before the TaskFragment has appeared.
-            final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask);
+            final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask);
             if (!taskContainer.setTaskBounds(taskBounds)) {
                 Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
             }
@@ -1119,6 +1125,10 @@
     }
 
     boolean launchPlaceholderIfNecessary(@NonNull Activity activity, boolean isOnCreated) {
+        if (activity.isFinishing()) {
+            return false;
+        }
+
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         // Don't launch placeholder if the container is occluded.
         if (container != null && container != getTopActiveContainer(container.getTaskId())) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 1b79ad9..a89847a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -65,6 +65,41 @@
     })
     private @interface Position {}
 
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * No need to expand the splitContainer because screen is big enough to
+     * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied.
+     */
+    static final int RESULT_NOT_EXPANDED = 0;
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * The splitContainer should be expanded. It is usually because minimum dimensions is not
+     * satisfied.
+     * @see #shouldShowSideBySide(Rect, SplitRule, Pair)
+     */
+    static final int RESULT_EXPANDED = 1;
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * The splitContainer should be expanded, but the client side hasn't received
+     * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer
+     * instead.
+     */
+    static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2;
+
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}
+     */
+    @IntDef(value = {
+            RESULT_NOT_EXPANDED,
+            RESULT_EXPANDED,
+            RESULT_EXPAND_FAILED_NO_TF_INFO,
+    })
+    private @interface ResultCode {}
+
     private final SplitController mController;
 
     SplitPresenter(@NonNull Executor executor, SplitController controller) {
@@ -396,6 +431,44 @@
         super.updateWindowingMode(wct, fragmentToken, windowingMode);
     }
 
+    /**
+     * Expands the split container if the current split bounds are smaller than the Activity or
+     * Intent that is added to the container.
+     *
+     * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)}
+     * and if {@link android.window.TaskFragmentInfo} has reported to the client side.
+     */
+    @ResultCode
+    int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
+            @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity,
+            @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) {
+        if (secondaryActivity == null && secondaryIntent == null) {
+            throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
+                    + " non-null.");
+        }
+        final Rect taskBounds = getParentContainerBounds(primaryActivity);
+        final Pair<Size, Size> minDimensionsPair;
+        if (secondaryActivity != null) {
+            minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
+        } else {
+            minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity,
+                    secondaryIntent);
+        }
+        // Expand the splitContainer if minimum dimensions are not satisfied.
+        if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) {
+            // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
+            // bounds. Return failure to create a new SplitContainer which fills task bounds.
+            if (splitContainer.getPrimaryContainer().getInfo() == null
+                    || splitContainer.getSecondaryContainer().getInfo() == null) {
+                return RESULT_EXPAND_FAILED_NO_TF_INFO;
+            }
+            expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken());
+            expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken());
+            return RESULT_EXPANDED;
+        }
+        return RESULT_NOT_EXPANDED;
+    }
+
     static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) {
         return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */);
     }
@@ -565,11 +638,19 @@
         if (container != null) {
             return getParentContainerBounds(container);
         }
-        return getTaskBoundsFromActivity(activity);
+        // Obtain bounds from Activity instead because the Activity hasn't been embedded yet.
+        return getNonEmbeddedActivityBounds(activity);
     }
 
+    /**
+     * Obtains the bounds from a non-embedded Activity.
+     * <p>
+     * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most
+     * cases unless we want to obtain task bounds before
+     * {@link TaskContainer#isTaskBoundsInitialized()}.
+     */
     @NonNull
-    static Rect getTaskBoundsFromActivity(@NonNull Activity activity) {
+    static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) {
         final WindowConfiguration windowConfiguration =
                 activity.getResources().getConfiguration().windowConfiguration;
         if (!activity.isInMultiWindowMode()) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 1ac3317..c4f3709 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -83,9 +83,9 @@
     }
 
     @Override
-    public void onAnimationCancelled() {
+    public void onAnimationCancelled(boolean isKeyguardOccluded) {
         if (TaskFragmentAnimationController.DEBUG) {
-            Log.v(TAG, "onAnimationCancelled");
+            Log.v(TAG, "onAnimationCancelled: isKeyguardOccluded=" + isKeyguardOccluded);
         }
         mHandler.post(this::cancelAnimation);
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index cfb3205..18086f5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -25,7 +25,10 @@
 
 import android.annotation.Nullable;
 import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityManager.AppTask;
 import android.app.Application;
+import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -180,7 +183,7 @@
         if (displayId != DEFAULT_DISPLAY) {
             Log.w(TAG, "This sample doesn't support display features on secondary displays");
             return features;
-        } else if (activity.isInMultiWindowMode()) {
+        } else if (isTaskInMultiWindowMode(activity)) {
             // It is recommended not to report any display features in multi-window mode, since it
             // won't be possible to synchronize the display feature positions with window movement.
             return features;
@@ -204,6 +207,32 @@
     }
 
     /**
+     * Checks whether the task associated with the activity is in multi-window. If task info is not
+     * available it defaults to {@code true}.
+     */
+    private boolean isTaskInMultiWindowMode(@NonNull Activity activity) {
+        final ActivityManager am = activity.getSystemService(ActivityManager.class);
+        if (am == null) {
+            return true;
+        }
+
+        final List<AppTask> appTasks = am.getAppTasks();
+        final int taskId = activity.getTaskId();
+        AppTask task = null;
+        for (AppTask t : appTasks) {
+            if (t.getTaskInfo().taskId == taskId) {
+                task = t;
+                break;
+            }
+        }
+        if (task == null) {
+            // The task might be removed on the server already.
+            return true;
+        }
+        return WindowConfiguration.inMultiWindowMode(task.getTaskInfo().getWindowingMode());
+    }
+
+    /**
      * Returns {@link true} if a {@link Rect} has zero width and zero height,
      * {@code false} otherwise.
      */
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 835c403..effc1a3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -24,6 +24,7 @@
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -57,13 +58,21 @@
     /** Creates a rule to always split the given activity and the given intent. */
     static SplitRule createSplitRule(@NonNull Activity primaryActivity,
             @NonNull Intent secondaryIntent) {
+        return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */);
+    }
+
+    /** Creates a rule to always split the given activity and the given intent. */
+    static SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent, boolean clearTop) {
         final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
         return new SplitPairRule.Builder(
                 activityPair -> false,
                 targetPair::equals,
                 w -> true)
                 .setSplitRatio(SPLIT_RATIO)
-                .setShouldClearTop(true)
+                .setShouldClearTop(clearTop)
+                .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+                .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
                 .build();
     }
 
@@ -75,6 +84,14 @@
                 true /* clearTop */);
     }
 
+    /** Creates a rule to always split the given activities. */
+    static SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
+        return createSplitRule(primaryActivity, secondaryActivity,
+                DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY,
+                clearTop);
+    }
+
     /** Creates a rule to always split the given activities with the given finish behaviors. */
     static SplitRule createSplitRule(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
@@ -105,4 +122,12 @@
                 false /* isTaskFragmentClearedForPip */,
                 new Point());
     }
+
+    static ActivityInfo createActivityInfoWithMinDimensions() {
+        ActivityInfo aInfo = new ActivityInfo();
+        final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
+        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
+                primaryBounds.width() + 1, primaryBounds.height() + 1);
+        return aInfo;
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index ef7728c..042547f 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -22,6 +22,7 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -34,6 +35,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
@@ -436,6 +438,50 @@
     }
 
     @Test
+    public void testResolveStartActivityIntent_shouldExpandSplitContainer() {
+        final Intent intent = new Intent().setComponent(
+                new ComponentName(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class));
+        setupSplitRule(mActivity, intent, false /* clearTop */);
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
+        assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
+        assertTrue(container.areLastRequestedBoundsEqual(null));
+        assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity));
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() {
+        final Intent intent = new Intent().setComponent(
+                new ComponentName(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class));
+        setupSplitRule(mActivity, intent, false /* clearTop */);
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+        final TaskFragmentContainer secondaryContainer = mSplitController
+                .getContainerWithActivity(secondaryActivity);
+        secondaryContainer.mInfo = null;
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
+        assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
+        assertTrue(container.areLastRequestedBoundsEqual(null));
+        assertNotEquals(container, secondaryContainer);
+    }
+
+    @Test
     public void testPlaceActivityInTopContainer() {
         mSplitController.placeActivityInTopContainer(mActivity);
 
@@ -787,11 +833,7 @@
         final Activity activityBelow = createMockActivity();
         setupSplitRule(mActivity, activityBelow);
 
-        ActivityInfo aInfo = new ActivityInfo();
-        final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
-        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
-                primaryBounds.width() + 1, primaryBounds.height() + 1);
-        doReturn(aInfo).when(mActivity).getActivityInfo();
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
 
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
@@ -810,17 +852,12 @@
         final Activity activityBelow = createMockActivity();
         setupSplitRule(activityBelow, mActivity);
 
-        ActivityInfo aInfo = new ActivityInfo();
-        final Rect secondaryBounds = getSplitBounds(false /* isPrimary */);
-        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
-                secondaryBounds.width() + 1, secondaryBounds.height() + 1);
-        doReturn(aInfo).when(mActivity).getActivityInfo();
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
 
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
 
-        // Allow to split as primary.
         boolean result = mSplitController.resolveActivityToContainer(mActivity,
                 false /* isOnReparent */);
 
@@ -828,6 +865,29 @@
         assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */);
     }
 
+    // Suppress GuardedBy warning on unit tests
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() {
+        final Activity primaryActivity = createMockActivity();
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */);
+
+        setupSplitRule(primaryActivity, mActivity, false /* clearTop */);
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
+        doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity));
+
+        clearInvocations(mSplitPresenter);
+        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* isOnReparent */);
+
+        assertTrue(result);
+        assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
+        assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
+                mSplitController.getContainerWithActivity(mActivity));
+        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any());
+    }
+
     @Test
     public void testResolveActivityToContainer_inUnknownTaskFragment() {
         doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
@@ -944,23 +1004,41 @@
     /** Setups a rule to always split the given activities. */
     private void setupSplitRule(@NonNull Activity primaryActivity,
             @NonNull Intent secondaryIntent) {
-        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent);
+        setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */);
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent, boolean clearTop) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop);
         mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
     }
 
     /** Setups a rule to always split the given activities. */
     private void setupSplitRule(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
-        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity);
+        setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */);
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop);
         mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
     }
 
     /** Adds a pair of TaskFragments as split for the given activities. */
     private void addSplitTaskFragments(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
+        addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */);
+    }
+
+    /** Adds a pair of TaskFragments as split for the given activities. */
+    private void addSplitTaskFragments(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
         registerSplitPair(createMockTaskFragmentContainer(primaryActivity),
                 createMockTaskFragmentContainer(secondaryActivity),
-                createSplitRule(primaryActivity, secondaryActivity));
+                createSplitRule(primaryActivity, secondaryActivity, clearTop));
     }
 
     /** Registers the two given TaskFragments as split pair. */
@@ -1011,16 +1089,18 @@
         if (primaryContainer.mInfo != null) {
             final Rect primaryBounds = matchParentBounds ? new Rect()
                     : getSplitBounds(true /* isPrimary */);
+            final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED
+                    : WINDOWING_MODE_MULTI_WINDOW;
             assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds));
-            assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(
-                    WINDOWING_MODE_MULTI_WINDOW));
+            assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode));
         }
         if (secondaryContainer.mInfo != null) {
             final Rect secondaryBounds = matchParentBounds ? new Rect()
                     : getSplitBounds(false /* isPrimary */);
+            final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED
+                    : WINDOWING_MODE_MULTI_WINDOW;
             assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds));
-            assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(
-                    WINDOWING_MODE_MULTI_WINDOW));
+            assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode));
         }
     }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index acc398a..d7931966 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -20,11 +20,16 @@
 
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED;
 import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition;
 import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
 import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
@@ -34,6 +39,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -49,6 +55,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
 import android.util.Size;
@@ -195,6 +202,52 @@
                         splitRule, mActivity, minDimensionsPair));
     }
 
+    @Test
+    public void testExpandSplitContainerIfNeeded() {
+        SplitContainer splitContainer = mock(SplitContainer.class);
+        Activity secondaryActivity = createMockActivity();
+        SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
+        TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
+        TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID);
+        doReturn(splitRule).when(splitContainer).getSplitRule();
+        doReturn(primaryTf).when(splitContainer).getPrimaryContainer();
+        doReturn(secondaryTf).when(splitContainer).getSecondaryContainer();
+
+        assertThrows(IllegalArgumentException.class, () ->
+                mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
+                        null /* secondaryActivity */, null /* secondaryIntent */));
+
+        assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
+        verify(mPresenter, never()).expandTaskFragment(any(), any());
+
+        doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
+        assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
+                mTransaction, splitContainer, mActivity, secondaryActivity,
+                null /* secondaryIntent */));
+
+        primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity));
+        secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
+
+        assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(primaryTf.getTaskFragmentToken()));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(secondaryTf.getTaskFragmentToken()));
+
+        clearInvocations(mPresenter);
+
+        assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, null /* secondaryActivity */,
+                new Intent(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class)));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(primaryTf.getTaskFragmentToken()));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(secondaryTf.getTaskFragmentToken()));
+    }
+
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
         final Configuration activityConfig = new Configuration();
@@ -203,6 +256,7 @@
         doReturn(mActivityResources).when(activity).getResources();
         doReturn(activityConfig).when(mActivityResources).getConfiguration();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
+        doReturn(mock(IBinder.class)).when(activity).getActivityToken();
         return activity;
     }
 }
diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml
index 86ca655..cc0333e 100644
--- a/libs/WindowManager/Shell/res/values-television/config.xml
+++ b/libs/WindowManager/Shell/res/values-television/config.xml
@@ -43,4 +43,13 @@
     <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
     if a custom action is present before closing it. -->
     <integer name="config_pipForceCloseDelay">5000</integer>
+
+    <!-- Animation duration when exit starting window: fade out icon -->
+    <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer>
+
+    <!-- Animation duration when exit starting window: reveal app -->
+    <integer name="starting_window_app_reveal_anim_delay">0</integer>
+
+    <!-- Animation duration when exit starting window: reveal app -->
+    <integer name="starting_window_app_reveal_anim_duration">0</integer>
 </resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index b2f0989..68a08513 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -157,7 +157,7 @@
     <string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
 
     <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
-    <string name="restart_button_description">Tap to restart this app and go full screen.</string>
+    <string name="restart_button_description">Tap to restart this app for a better view.</string>
 
     <!-- Description of the camera compat button for applying stretched issues treatment in the hint for
          compatibility control. [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index e71a59d..8c0affb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -31,13 +31,15 @@
     /**
      * Called when a {@link MotionEvent} is generated by a back gesture.
      *
-     * @param event the original {@link MotionEvent}
-     * @param action the original {@link KeyEvent#getAction()} when the event was dispatched to
+     * @param touchX the X touch position of the {@link MotionEvent}.
+     * @param touchY the Y touch position of the {@link MotionEvent}.
+     * @param keyAction the original {@link KeyEvent#getAction()} when the event was dispatched to
      *               the process. This is forwarded separately because the input pipeline may mutate
      *               the {#event} action state later.
      * @param swipeEdge the edge from which the swipe begins.
      */
-    void onBackMotion(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge);
+    void onBackMotion(float touchX, float touchY, int keyAction,
+            @BackEvent.SwipeEdge int swipeEdge);
 
     /**
      * Sets whether the back gesture is past the trigger threshold or not.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 0cb56d7..0cf2b28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -184,8 +184,8 @@
 
         @Override
         public void onBackMotion(
-                MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) {
-            mShellExecutor.execute(() -> onMotionEvent(event, action, swipeEdge));
+                float touchX, float touchY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) {
+            mShellExecutor.execute(() -> onMotionEvent(touchX, touchY, keyAction, swipeEdge));
         }
 
         @Override
@@ -256,33 +256,34 @@
      * Called when a new motion event needs to be transferred to this
      * {@link BackAnimationController}
      */
-    public void onMotionEvent(MotionEvent event, int action, @BackEvent.SwipeEdge int swipeEdge) {
+    public void onMotionEvent(float touchX, float touchY, int keyAction,
+            @BackEvent.SwipeEdge int swipeEdge) {
         if (mTransitionInProgress) {
             return;
         }
-        if (action == MotionEvent.ACTION_MOVE) {
+        if (keyAction == MotionEvent.ACTION_MOVE) {
             if (!mBackGestureStarted) {
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                 // could be happened when ACTION_DOWN, it may change the current focus that we
                 // would access it when startBackNavigation.
-                initAnimation(event);
+                initAnimation(touchX, touchY);
             }
-            onMove(event, swipeEdge);
-        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            onMove(touchX, touchY, swipeEdge);
+        } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
             ProtoLog.d(WM_SHELL_BACK_PREVIEW,
-                    "Finishing gesture with event action: %d", action);
+                    "Finishing gesture with event action: %d", keyAction);
             onGestureFinished();
         }
     }
 
-    private void initAnimation(MotionEvent event) {
+    private void initAnimation(float touchX, float touchY) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
         if (mBackGestureStarted || mBackNavigationInfo != null) {
             Log.e(TAG, "Animation is being initialized but is already started.");
             finishAnimation();
         }
 
-        mInitTouchLocation.set(event.getX(), event.getY());
+        mInitTouchLocation.set(touchX, touchY);
         mBackGestureStarted = true;
 
         try {
@@ -351,18 +352,18 @@
         mTransaction.setVisibility(screenshotSurface, true);
     }
 
-    private void onMove(MotionEvent event, @BackEvent.SwipeEdge int swipeEdge) {
+    private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
         if (!mBackGestureStarted || mBackNavigationInfo == null) {
             return;
         }
-        int deltaX = Math.round(event.getX() - mInitTouchLocation.x);
+        int deltaX = Math.round(touchX - mInitTouchLocation.x);
         float progressThreshold = PROGRESS_THRESHOLD >= 0 ? PROGRESS_THRESHOLD : mProgressThreshold;
         float progress = Math.min(Math.max(Math.abs(deltaX) / progressThreshold, 0), 1);
         int backType = mBackNavigationInfo.getType();
         RemoteAnimationTarget animationTarget = mBackNavigationInfo.getDepartingAnimationTarget();
 
         BackEvent backEvent = new BackEvent(
-                event.getX(), event.getY(), progress, swipeEdge, animationTarget);
+                touchX, touchY, progress, swipeEdge, animationTarget);
         IOnBackInvokedCallback targetCallback = null;
         if (shouldDispatchToLauncher(backType)) {
             targetCallback = mBackToLauncherCallback;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 1e36989..a8c1071 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -68,15 +68,15 @@
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
 import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
 import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
-import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
 import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
+import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
 import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
@@ -218,6 +218,7 @@
             PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState,
             PipMotionHelper pipMotionHelper, PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
+            PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
             TaskStackListenerImpl taskStackListener,
@@ -227,7 +228,7 @@
         return Optional.ofNullable(PipController.create(context, displayController,
                 pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState,
                 pipMotionHelper,
-                pipMediaController, phonePipMenuController, pipTaskOrganizer,
+                pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
                 pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
                 taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor));
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 3b3091a..bbc47e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -86,12 +86,12 @@
     }
 
     /**
-     * Registers the pinned stack animation listener.
+     * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
      *
-     * @param callback The callback of pinned stack animation.
+     * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
+     *                 when it's changed.
      */
-    default void setPinnedStackAnimationListener(Consumer<Boolean> callback) {
-    }
+    default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
 
     /**
      * Set the pinned stack with {@link PipAnimationController.AnimationType}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 4eba169..cf2734c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -591,7 +591,7 @@
                         final Rect insets = computeInsets(fraction);
                         getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
                                 sourceHintRect, initialSourceValue, bounds, insets,
-                                isInPipDirection);
+                                isInPipDirection, fraction);
                         if (shouldApplyCornerRadius()) {
                             final Rect sourceBounds = new Rect(initialContainerRect);
                             sourceBounds.inset(insets);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index a017a26..c0bc108 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -104,7 +104,7 @@
     public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx,
             SurfaceControl leash, Rect sourceRectHint,
             Rect sourceBounds, Rect destinationBounds, Rect insets,
-            boolean isInPipDirection) {
+            boolean isInPipDirection, float fraction) {
         mTmpDestinationRect.set(sourceBounds);
         // Similar to {@link #scale}, we want to position the surface relative to the screen
         // coordinates so offset the bounds to 0,0
@@ -116,9 +116,13 @@
         if (isInPipDirection
                 && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
             // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
-            scale = sourceBounds.width() <= sourceBounds.height()
+            final float endScale = sourceBounds.width() <= sourceBounds.height()
                     ? (float) destinationBounds.width() / sourceRectHint.width()
                     : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceBounds.width() <= sourceBounds.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - fraction) * startScale + fraction * endScale;
         } else {
             scale = sourceBounds.width() <= sourceBounds.height()
                     ? (float) destinationBounds.width() / sourceBounds.width()
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 bd386b5..22b0ccb 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
@@ -942,7 +942,7 @@
         // Re-set the PIP bounds to none.
         mPipBoundsState.setBounds(new Rect());
         mPipUiEventLoggerLogger.setTaskInfo(null);
-        mPipMenuController.detach();
+        mMainExecutor.executeDelayed(() -> mPipMenuController.detach(), 0);
 
         if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) {
             mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY);
@@ -1472,6 +1472,11 @@
                     "%s: Abort animation, invalid leash", TAG);
             return null;
         }
+        if (isInPipDirection(direction)
+                && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) {
+            // The given source rect hint is too small for enter PiP animation, reset it to null.
+            sourceHintRect = null;
+        }
         final int rotationDelta = mWaitForFixedRotation
                 ? deltaRotation(mCurrentRotation, mNextRotation)
                 : Surface.ROTATION_0;
@@ -1546,6 +1551,20 @@
     }
 
     /**
+     * This is a situation in which the source rect hint on at least one axis is smaller
+     * than the destination bounds, which represents a problem because we would have to scale
+     * up that axis to fit the bounds. So instead, just fallback to the non-source hint
+     * animation in this case.
+     *
+     * @return {@code false} if the given source is too small to use for the entering animation.
+     */
+    private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
+        return sourceRectHint != null
+                && sourceRectHint.width() > destinationBounds.width()
+                && sourceRectHint.height() > destinationBounds.height();
+    }
+
+    /**
      * Sync with {@link SplitScreenController} on destination bounds if PiP is going to
      * split screen.
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 28427a8..05a890f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -42,6 +42,7 @@
 import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
 import static com.android.wm.shell.transition.Transitions.isOpeningType;
 
+import android.animation.Animator;
 import android.app.ActivityManager;
 import android.app.TaskInfo;
 import android.content.Context;
@@ -248,6 +249,13 @@
         return false;
     }
 
+    @Override
+    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        end();
+    }
+
     /** Helper to identify whether this handler is currently the one playing an animation */
     private boolean isAnimatingLocally() {
         return mFinishTransaction != null;
@@ -283,6 +291,13 @@
     }
 
     @Override
+    public void end() {
+        Animator animator = mPipAnimationController.getCurrentAnimator();
+        if (animator == null) return;
+        animator.end();
+    }
+
+    @Override
     public boolean handleRotateDisplay(int startRotation, int endRotation,
             WindowContainerTransaction wct) {
         if (mRequestedEnterTransition != null && mOneShotAnimationType == ANIM_TYPE_ALPHA) {
@@ -700,7 +715,7 @@
         mSurfaceTransactionHelper
                 .crop(finishTransaction, leash, destinationBounds)
                 .round(finishTransaction, leash, true /* applyCornerRadius */);
-        mPipMenuController.attach(leash);
+        mTransitions.getMainExecutor().executeDelayed(() -> mPipMenuController.attach(leash), 0);
 
         if (taskInfo.pictureInPictureParams != null
                 && taskInfo.pictureInPictureParams.isAutoEnterEnabled()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index d3f69f6..90a2695 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -28,9 +28,7 @@
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
-import android.os.Handler;
 import android.os.IBinder;
-import android.os.Looper;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -56,7 +54,6 @@
     protected final ShellTaskOrganizer mShellTaskOrganizer;
     protected final PipMenuController mPipMenuController;
     protected final Transitions mTransitions;
-    private final Handler mMainHandler;
     private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
     protected PipTaskOrganizer mPipOrganizer;
 
@@ -144,7 +141,6 @@
         mPipBoundsAlgorithm = pipBoundsAlgorithm;
         mPipAnimationController = pipAnimationController;
         mTransitions = transitions;
-        mMainHandler = new Handler(Looper.getMainLooper());
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             transitions.addHandler(this);
         }
@@ -237,6 +233,10 @@
             @NonNull final Transitions.TransitionFinishCallback finishCallback) {
     }
 
+    /** End the currently-playing PiP animation. */
+    public void end() {
+    }
+
     /**
      * Callback interface for PiP transitions (both from and to PiP mode)
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
index 85e56b7..1a4be3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java
@@ -17,12 +17,15 @@
 package com.android.wm.shell.pip;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.pm.ActivityInfo;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and
@@ -37,6 +40,9 @@
     public static final int ENTERED_PIP = 4;
     public static final int EXITING_PIP = 5;
 
+    private final List<OnPipTransitionStateChangedListener> mOnPipTransitionStateChangedListeners =
+            new ArrayList<>();
+
     /**
      * If set to {@code true}, no entering PiP transition would be kicked off and most likely
      * it's due to the fact that Launcher is handling the transition directly when swiping
@@ -65,7 +71,13 @@
     }
 
     public void setTransitionState(@TransitionState int state) {
-        mState = state;
+        if (mState != state) {
+            for (int i = 0; i < mOnPipTransitionStateChangedListeners.size(); i++) {
+                mOnPipTransitionStateChangedListeners.get(i).onPipTransitionStateChanged(
+                        mState, state);
+            }
+            mState = state;
+        }
     }
 
     public @TransitionState int getTransitionState() {
@@ -73,8 +85,7 @@
     }
 
     public boolean isInPip() {
-        return mState >= TASK_APPEARED
-                && mState != EXITING_PIP;
+        return isInPip(mState);
     }
 
     public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) {
@@ -94,4 +105,23 @@
         return mState < ENTERING_PIP
                 || mState == EXITING_PIP;
     }
+
+    public void addOnPipTransitionStateChangedListener(
+            @NonNull OnPipTransitionStateChangedListener listener) {
+        mOnPipTransitionStateChangedListeners.add(listener);
+    }
+
+    public void removeOnPipTransitionStateChangedListener(
+            @NonNull OnPipTransitionStateChangedListener listener) {
+        mOnPipTransitionStateChangedListeners.remove(listener);
+    }
+
+    public static boolean isInPip(@TransitionState int state) {
+        return state >= TASK_APPEARED && state != EXITING_PIP;
+    }
+
+    public interface OnPipTransitionStateChangedListener {
+        void onPipTransitionStateChanged(@TransitionState int oldState,
+                @TransitionState int newState);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index c3e6d82..3000998 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -84,6 +84,7 @@
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUtils;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.transition.Transitions;
@@ -128,11 +129,14 @@
 
     protected PhonePipMenuController mMenuController;
     protected PipTaskOrganizer mPipTaskOrganizer;
+    private PipTransitionState mPipTransitionState;
     protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener =
             new PipControllerPinnedTaskListener();
 
     private boolean mIsKeyguardShowingOrAnimating;
 
+    private Consumer<Boolean> mOnIsInPipStateChangedListener;
+
     private interface PipAnimationListener {
         /**
          * Notifies the listener that the Pip animation is started.
@@ -291,6 +295,7 @@
             PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState,
             PipMotionHelper pipMotionHelper, PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
+            PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
             TaskStackListenerImpl taskStackListener,
@@ -305,7 +310,8 @@
 
         return new PipController(context, displayController, pipAppOpsListener, pipBoundsAlgorithm,
                 pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper, pipMediaController,
-                phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController,
+                phonePipMenuController, pipTaskOrganizer, pipTransitionState,
+                pipTouchHandler, pipTransitionController,
                 windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
                 oneHandedController, mainExecutor)
                 .mImpl;
@@ -321,6 +327,7 @@
             PipMediaController pipMediaController,
             PhonePipMenuController phonePipMenuController,
             PipTaskOrganizer pipTaskOrganizer,
+            PipTransitionState pipTransitionState,
             PipTouchHandler pipTouchHandler,
             PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
@@ -344,6 +351,7 @@
         mPipBoundsState = pipBoundsState;
         mPipMotionHelper = pipMotionHelper;
         mPipTaskOrganizer = pipTaskOrganizer;
+        mPipTransitionState = pipTransitionState;
         mMainExecutor = mainExecutor;
         mMediaController = pipMediaController;
         mMenuController = phonePipMenuController;
@@ -370,6 +378,15 @@
             onDisplayChanged(mDisplayController.getDisplayLayout(displayId),
                     false /* saveRestoreSnapFraction */);
         });
+        mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> {
+            if (mOnIsInPipStateChangedListener != null) {
+                final boolean wasInPip = PipTransitionState.isInPip(oldState);
+                final boolean nowInPip = PipTransitionState.isInPip(newState);
+                if (nowInPip != wasInPip) {
+                    mOnIsInPipStateChangedListener.accept(nowInPip);
+                }
+            }
+        });
         mPipBoundsState.setOnMinimalSizeChangeCallback(
                 () -> {
                     // The minimal size drives the normal bounds, so they need to be recalculated.
@@ -664,6 +681,13 @@
         }
     }
 
+    private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+        mOnIsInPipStateChangedListener = callback;
+        if (mOnIsInPipStateChangedListener != null) {
+            callback.accept(mPipTransitionState.isInPip());
+        }
+    }
+
     private void setShelfHeightLocked(boolean visible, int height) {
         final int shelfHeight = visible ? height : 0;
         mPipBoundsState.setShelfVisibility(visible, shelfHeight);
@@ -941,6 +965,13 @@
         }
 
         @Override
+        public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
+            mMainExecutor.execute(() -> {
+                PipController.this.setOnIsInPipStateChangedListener(callback);
+            });
+        }
+
+        @Override
         public void setPinnedStackAnimationType(int animationType) {
             mMainExecutor.execute(() -> {
                 PipController.this.setPinnedStackAnimationType(animationType);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index f7057d4..e55729a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -225,11 +225,27 @@
 
     void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t,
             IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) {
-        if (mergeTarget == mAnimatingTransition && mActiveRemoteHandler != null) {
+        if (mergeTarget != mAnimatingTransition) return;
+        if (mActiveRemoteHandler != null) {
             mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+        } else {
+            for (int i = mAnimations.size() - 1; i >= 0; --i) {
+                final Animator anim = mAnimations.get(i);
+                mTransitions.getAnimExecutor().execute(anim::end);
+            }
         }
     }
 
+    boolean end() {
+        // If its remote, there's nothing we can do right now.
+        if (mActiveRemoteHandler != null) return false;
+        for (int i = mAnimations.size() - 1; i >= 0; --i) {
+            final Animator anim = mAnimations.get(i);
+            mTransitions.getAnimExecutor().execute(anim::end);
+        }
+        return true;
+    }
+
     void onTransitionMerged(@NonNull IBinder transition) {
         // Once a pending enter transition got merged, make sure to append the reset of finishing
         // operations to the finish transition.
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 6cfb700..59b0afe 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
@@ -457,10 +457,10 @@
             }
 
             @Override
-            public void onAnimationCancelled() {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 onRemoteAnimationFinishedOrCancelled(evictWct);
                 try {
-                    adapter.getRunner().onAnimationCancelled();
+                    adapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
                 } catch (RemoteException e) {
                     Slog.e(TAG, "Error starting remote animation", e);
                 }
@@ -1521,6 +1521,11 @@
         mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
     }
 
+    /** Jump the current transition animation to the end. */
+    public boolean end() {
+        return mSplitTransitions.end();
+    }
+
     @Override
     public void onTransitionMerged(@NonNull IBinder transition) {
         mSplitTransitions.onTransitionMerged(transition);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 95bc579..19d3acb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -20,10 +20,8 @@
 import static android.graphics.Color.WHITE;
 import static android.graphics.Color.alpha;
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
-import static android.view.ViewRootImpl.LOCAL_LAYOUT;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static android.view.WindowLayout.UNSPECIFIED_LENGTH;
 import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
 import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
@@ -53,7 +51,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.TaskDescription;
 import android.app.ActivityThread;
-import android.app.WindowConfiguration;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -80,7 +77,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
-import android.view.WindowLayout;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.window.ClientWindowFrames;
@@ -212,8 +208,6 @@
         final IWindowSession session = WindowManagerGlobal.getWindowSession();
         final SurfaceControl surfaceControl = new SurfaceControl();
         final ClientWindowFrames tmpFrames = new ClientWindowFrames();
-        final WindowLayout windowLayout = new WindowLayout();
-        final Rect displayCutoutSafe = new Rect();
 
         final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0];
         final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration();
@@ -238,7 +232,8 @@
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay");
             final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId,
-                    info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls);
+                    info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls,
+                    new Rect());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
             if (res < 0) {
                 Slog.w(TAG, "Failed to add snapshot starting window res=" + res);
@@ -250,25 +245,9 @@
         window.setOuter(snapshotSurface);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
-            if (LOCAL_LAYOUT) {
-                if (!surfaceControl.isValid()) {
-                    session.updateVisibility(window, layoutParams, View.VISIBLE,
-                            tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls);
-                }
-                tmpInsetsState.getDisplayCutoutSafe(displayCutoutSafe);
-                final WindowConfiguration winConfig =
-                        tmpMergedConfiguration.getMergedConfiguration().windowConfiguration;
-                windowLayout.computeFrames(layoutParams, tmpInsetsState, displayCutoutSafe,
-                        winConfig.getBounds(), winConfig.getWindowingMode(), UNSPECIFIED_LENGTH,
-                        UNSPECIFIED_LENGTH, info.requestedVisibilities,
-                        null /* attachedWindowFrame */, 1f /* compatScale */, tmpFrames);
-                session.updateLayout(window, layoutParams, 0 /* flags */, tmpFrames,
-                        UNSPECIFIED_LENGTH, UNSPECIFIED_LENGTH);
-            } else {
-                session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0,
-                        tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
-                        tmpControls, new Bundle());
-            }
+            session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0,
+                    tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
+                    tmpControls, new Bundle());
             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
         } catch (RemoteException e) {
             snapshotSurface.clearWindowSynced();
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 1ffe26df..7234d55 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
@@ -53,10 +53,18 @@
     private static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
+        /** The default animation for this mixed transition. */
+        static final int ANIM_TYPE_DEFAULT = 0;
+
+        /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */
+        static final int ANIM_TYPE_GOING_HOME = 1;
+
         final int mType;
+        int mAnimType = 0;
         final IBinder mTransition;
 
         Transitions.TransitionFinishCallback mFinishCallback = null;
+        Transitions.TransitionHandler mLeftoversHandler = null;
 
         /**
          * Mixed transitions are made up of multiple "parts". This keeps track of how many
@@ -128,7 +136,7 @@
         MixedTransition mixed = null;
         for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
             if (mActiveTransitions.get(i).mTransition != transition) continue;
-            mixed = mActiveTransitions.remove(i);
+            mixed = mActiveTransitions.get(i);
             break;
         }
         if (mixed == null) return false;
@@ -137,6 +145,7 @@
             return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
                     finishCallback);
         } else {
+            mActiveTransitions.remove(mixed);
             throw new IllegalStateException("Starting mixed animation without a known mixed type? "
                     + mixed.mType);
         }
@@ -178,6 +187,7 @@
         Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
             --mixed.mInFlightSubAnimations;
             if (mixed.mInFlightSubAnimations > 0) return;
+            mActiveTransitions.remove(mixed);
             if (isGoingHome) {
                 mSplitHandler.onTransitionAnimationComplete();
             }
@@ -216,8 +226,8 @@
                     finishCB);
             // Dispatch the rest of the transition normally. This will most-likely be taken by
             // recents or default handler.
-            mPlayer.dispatchTransition(mixed.mTransition, everythingElse, otherStartT,
-                    finishTransaction, finishCB, this);
+            mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse,
+                    otherStartT, finishTransaction, finishCB, this);
         } else {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
                     + "forward animation to Pip-Handler.");
@@ -235,6 +245,32 @@
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        for (int i = 0; i < mActiveTransitions.size(); ++i) {
+            if (mActiveTransitions.get(i) != mergeTarget) continue;
+            MixedTransition mixed = mActiveTransitions.get(i);
+            if (mixed.mInFlightSubAnimations <= 0) {
+                // Already done, so no need to end it.
+                return;
+            }
+            if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
+                if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
+                    boolean ended = mSplitHandler.end();
+                    // If split couldn't end (because it is remote), then don't end everything else
+                    // since we have to play out the animation anyways.
+                    if (!ended) return;
+                    mPipHandler.end();
+                    if (mixed.mLeftoversHandler != null) {
+                        mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                                finishCallback);
+                    }
+                } else {
+                    mPipHandler.end();
+                }
+            } else {
+                throw new IllegalStateException("Playing a mixed transition with unknown type? "
+                        + mixed.mType);
+            }
+        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index c3eaa8e..dcd6277 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -523,6 +523,18 @@
         return true;
     }
 
+    @Override
+    public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ArrayList<Animator> anims = mAnimations.get(mergeTarget);
+        if (anims == null) return;
+        for (int i = anims.size() - 1; i >= 0; --i) {
+            final Animator anim = anims.get(i);
+            mAnimExecutor.execute(anim::end);
+        }
+    }
+
     private void edgeExtendWindow(TransitionInfo.Change change,
             Animation a, SurfaceControl.Transaction startTransaction,
             SurfaceControl.Transaction finishTransaction) {
@@ -854,13 +866,19 @@
             });
         };
         va.addListener(new AnimatorListenerAdapter() {
+            private boolean mFinished = false;
+
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (mFinished) return;
+                mFinished = true;
                 finisher.run();
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
+                if (mFinished) return;
+                mFinished = true;
                 finisher.run();
             }
         });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
index 61e11e8..61e92f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -107,7 +107,7 @@
             }
 
             @Override
-            public void onAnimationCancelled() throws RemoteException {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
                 mCancelled = true;
                 mApps = mWallpapers = mNonApps = null;
                 checkApply();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index fcfcbfa..e7c5cb2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -298,7 +298,7 @@
 
     private void doMotionEvent(int actionDown, int coordinate) {
         mController.onMotionEvent(
-                MotionEvent.obtain(0, mEventTime, actionDown, coordinate, coordinate, 0),
+                coordinate, coordinate,
                 actionDown,
                 BackEvent.EDGE_LEFT);
         mEventTime += 10;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index abd55dd..babc970 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -53,6 +53,7 @@
 import com.android.wm.shell.pip.PipSnapAlgorithm;
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -80,6 +81,7 @@
     @Mock private PipSnapAlgorithm mMockPipSnapAlgorithm;
     @Mock private PipMediaController mMockPipMediaController;
     @Mock private PipTaskOrganizer mMockPipTaskOrganizer;
+    @Mock private PipTransitionState mMockPipTransitionState;
     @Mock private PipTransitionController mMockPipTransitionController;
     @Mock private PipTouchHandler mMockPipTouchHandler;
     @Mock private PipMotionHelper mMockPipMotionHelper;
@@ -104,8 +106,8 @@
                 mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
                 mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
-                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
-                mMockPipTransitionController, mMockWindowManagerShellWrapper,
+                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
+                mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
                 mMockTaskStackListener, mPipParamsChangedForwarder,
                 mMockOneHandedController, mMockExecutor);
         when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
@@ -138,8 +140,8 @@
                 mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
                 mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
-                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTouchHandler,
-                mMockPipTransitionController, mMockWindowManagerShellWrapper,
+                mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
+                mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
                 mMockTaskStackListener, mPipParamsChangedForwarder,
                 mMockOneHandedController, mMockExecutor));
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index 630d0d2..14d8ce4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -249,7 +249,8 @@
                 any() /* window */, any() /* attrs */,
                 anyInt() /* viewVisibility */, anyInt() /* displayId */,
                 any() /* requestedVisibility */, any() /* outInputChannel */,
-                any() /* outInsetsState */, any() /* outActiveControls */);
+                any() /* outInsetsState */, any() /* outActiveControls */,
+                any() /* outAttachedFrame */);
         TaskSnapshotWindow mockSnapshotWindow = TaskSnapshotWindow.create(windowInfo,
                 mBinder,
                 snapshot, mTestExecutor, () -> {
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl
index b136d5b..2bdd5c8 100644
--- a/media/java/android/media/projection/IMediaProjection.aidl
+++ b/media/java/android/media/projection/IMediaProjection.aidl
@@ -17,7 +17,7 @@
 package android.media.projection;
 
 import android.media.projection.IMediaProjectionCallback;
-import android.window.WindowContainerToken;
+import android.os.IBinder;
 
 /** {@hide} */
 interface IMediaProjection {
@@ -31,14 +31,14 @@
     void unregisterCallback(IMediaProjectionCallback callback);
 
     /**
-     * Returns the {@link android.window.WindowContainerToken} identifying the task to record, or
-     * {@code null} if there is none.
+     * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * there is none.
      */
-    WindowContainerToken getTaskRecordingWindowContainerToken();
+    IBinder getLaunchCookie();
 
     /**
-     * Updates the {@link android.window.WindowContainerToken} identifying the task to record, or
-     * {@code null} if there is none.
+     * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if
+     * there is none.
      */
-    void setTaskRecordingWindowContainerToken(in WindowContainerToken token);
+    void setLaunchCookie(in IBinder launchCookie);
 }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index ba7bf3f..ae44fc5 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -25,13 +25,13 @@
 import android.hardware.display.VirtualDisplay;
 import android.hardware.display.VirtualDisplayConfig;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.ContentRecordingSession;
 import android.view.Surface;
-import android.window.WindowContainerToken;
 
 import java.util.Map;
 
@@ -172,18 +172,16 @@
             @NonNull VirtualDisplayConfig.Builder virtualDisplayConfig,
             @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) {
         try {
-            final WindowContainerToken taskWindowContainerToken =
-                    mImpl.getTaskRecordingWindowContainerToken();
+            final IBinder launchCookie = mImpl.getLaunchCookie();
             Context windowContext = null;
             ContentRecordingSession session;
-            if (taskWindowContainerToken == null) {
+            if (launchCookie == null) {
                 windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(),
                         TYPE_APPLICATION, null /* options */);
                 session = ContentRecordingSession.createDisplaySession(
                         windowContext.getWindowContextToken());
             } else {
-                session = ContentRecordingSession.createTaskSession(
-                        taskWindowContainerToken.asBinder());
+                session = ContentRecordingSession.createTaskSession(launchCookie);
             }
             virtualDisplayConfig.setWindowManagerMirroring(true);
             final DisplayManager dm = mContext.getSystemService(DisplayManager.class);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 89e10c4..fc70ba4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -20,15 +20,19 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
+import android.os.Build;
 import android.os.ParcelUuid;
 import android.util.Log;
 
+import androidx.annotation.ChecksSdkIntAtLeast;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
@@ -126,32 +130,84 @@
         }
     }
 
+    @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
+    private static boolean isAtLeastT() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
+    }
+
     // Group devices by groupId
     @VisibleForTesting
     void onGroupIdChanged(int groupId) {
-        int firstMatchedIndex = -1;
-        CachedBluetoothDevice mainDevice = null;
+        if (!isValidGroupId(groupId)) {
+            log("onGroupIdChanged: groupId is invalid");
+            return;
+        }
+        log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString());
+        final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+        final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
+        final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
+        final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ?
+                leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
+        CachedBluetoothDevice newMainDevice =
+                mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null;
+        if (newMainDevice != null) {
+            final CachedBluetoothDevice finalNewMainDevice = newMainDevice;
+            final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream()
+                    .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice)
+                            && cachedDevice.getGroupId() == groupId)
+                    .collect(Collectors.toList());
+            if (memberDevices == null || memberDevices.isEmpty()) {
+                log("onGroupIdChanged: There is no member device in list.");
+                return;
+            }
+            log("onGroupIdChanged: removed from UI device =" + memberDevices
+                    + ", with groupId=" + groupId + " mainDevice= " + newMainDevice);
+            for (CachedBluetoothDevice memberDeviceItem : memberDevices) {
+                Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice();
+                if (!memberSet.isEmpty()) {
+                    log("onGroupIdChanged: Transfer the member list into new main device.");
+                    for (CachedBluetoothDevice memberListItem : memberSet) {
+                        if (!memberListItem.equals(newMainDevice)) {
+                            newMainDevice.addMemberDevice(memberListItem);
+                        }
+                    }
+                    memberSet.clear();
+                }
 
-        for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
-            final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
-            if (cachedDevice.getGroupId() != groupId) {
-                continue;
+                newMainDevice.addMemberDevice(memberDeviceItem);
+                mCachedDevices.remove(memberDeviceItem);
+                mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem);
             }
 
-            if (firstMatchedIndex == -1) {
-                // Found the first one
-                firstMatchedIndex = i;
-                mainDevice = cachedDevice;
-                continue;
+            if (!mCachedDevices.contains(newMainDevice)) {
+                mCachedDevices.add(newMainDevice);
+                mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice);
             }
+        } else {
+            log("onGroupIdChanged: There is no main device from the LE profile.");
+            int firstMatchedIndex = -1;
 
-            log("onGroupIdChanged: removed from UI device =" + cachedDevice
-                    + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
+            for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
+                final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
+                if (cachedDevice.getGroupId() != groupId) {
+                    continue;
+                }
 
-            mainDevice.addMemberDevice(cachedDevice);
-            mCachedDevices.remove(i);
-            mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
-            break;
+                if (firstMatchedIndex == -1) {
+                    // Found the first one
+                    firstMatchedIndex = i;
+                    newMainDevice = cachedDevice;
+                    continue;
+                }
+
+                log("onGroupIdChanged: removed from UI device =" + cachedDevice
+                        + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
+
+                newMainDevice.addMemberDevice(cachedDevice);
+                mCachedDevices.remove(i);
+                mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
+                break;
+            }
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 19df1e9..0f57d87 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -21,6 +21,7 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
+import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCodecConfig;
@@ -183,6 +184,37 @@
         return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
     }
 
+    /**
+     * Get Lead device for the group.
+     *
+     * Lead device is the device that can be used as an active device in the system.
+     * Active devices points to the Audio Device for the Le Audio group.
+     * This method returns the Lead devices for the connected LE Audio
+     * group and this device should be used in the setActiveDevice() method by other parts
+     * of the system, which wants to set to active a particular Le Audio group.
+     *
+     * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
+     * Note: When Lead device gets disconnected while Le Audio group is active and has more devices
+     * in the group, then Lead device will not change. If Lead device gets disconnected, for the
+     * Le Audio group which is not active, a new Lead device will be chosen
+     *
+     * @param groupId The group id.
+     * @return group lead device.
+     *
+     * @hide
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
+        if (DEBUG) {
+            Log.d(TAG,"getConnectedGroupLeadDevice");
+        }
+        if (mService == null) {
+            Log.e(TAG,"No service.");
+            return null;
+        }
+        return mService.getConnectedGroupLeadDevice(groupId);
+    }
+
     @Override
     public boolean isEnabled(BluetoothDevice device) {
         if (mService == null || device == null) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
index e8cbab8..a040e28 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java
@@ -32,7 +32,7 @@
     private static final String TAG = "MediaManager";
 
     protected final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
-    protected final List<MediaDevice> mMediaDevices = new ArrayList<>();
+    protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
 
     protected Context mContext;
     protected Notification mNotification;
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f05c1e2..fa87de2 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -93,6 +93,7 @@
         "SystemUISharedLib",
         "SystemUI-statsd",
         "SettingsLib",
+        "androidx.core_core-ktx",
         "androidx.viewpager2_viewpager2",
         "androidx.legacy_legacy-support-v4",
         "androidx.recyclerview_recyclerview",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 3bd6d51..f9a9ef6 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -52,6 +52,17 @@
       ]
     },
     {
+      "name": "SystemUIGoogleScreenshotTests",
+      "options": [
+        {
+          "exclude-annotation": "org.junit.Ignore"
+        },
+        {
+          "exclude-annotation": "androidx.test.filters.FlakyTest"
+        }
+      ]
+    },
+    {
       // Permission indicators
       "name": "CtsPermission4TestCases",
       "options": [
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
index 5b47ae5..fbe3356 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt
@@ -656,7 +656,7 @@
             controller.onLaunchAnimationCancelled()
         }
 
-        override fun onAnimationCancelled() {
+        override fun onAnimationCancelled(isKeyguardOccluded: Boolean) {
             if (timedOut) {
                 return
             }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
index 0f10589..bd628cc 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java
@@ -29,7 +29,7 @@
 /**
  * Interface that decides whether a touch on the phone was accidental. i.e. Pocket Dialing.
  *
- * {@see com.android.systemui.classifier.FalsingManagerImpl}
+ * {@see com.android.systemui.classifier.BrightLineFalsingManager}
  */
 @ProvidesInterface(version = FalsingManager.VERSION)
 public interface FalsingManager {
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index 77f1803..acf3e4d 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -102,12 +102,13 @@
          screen. -->
     <item name="half_opened_bouncer_height_ratio" type="dimen" format="float">0.0</item>
 
-    <!-- The actual amount of translation that is applied to the bouncer when it animates from one
-         side of the screen to the other in one-handed mode. Note that it will always translate from
-         the side of the screen to the other (it will "jump" closer to the destination while the
-         opacity is zero), but this controls how much motion will actually be applied to it while
-         animating. Larger values will cause it to move "faster" while fading out/in. -->
-    <dimen name="one_handed_bouncer_move_animation_translation">120dp</dimen>
+    <!-- The actual amount of translation that is applied to the security when it animates from one
+         side of the screen to the other in one-handed or user switcher mode. Note that it will
+         always translate from the side of the screen to the other (it will "jump" closer to the
+         destination while the opacity is zero), but this controls how much motion will actually be
+         applied to it while animating. Larger values will cause it to move "faster" while
+         fading out/in. -->
+    <dimen name="security_shift_animation_translation">120dp</dimen>
 
 
     <dimen name="bouncer_user_switcher_header_text_size">20sp</dimen>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
index 3f56baf..efbdd1a 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
@@ -20,5 +20,5 @@
     style="@style/clock_subtitle"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:format12Hour="EEE, MMM d"
-    android:format24Hour="EEE, MMM d"/>
+    android:format12Hour="@string/dream_date_complication_date_format"
+    android:format24Hour="@string/dream_date_complication_date_format"/>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index e066d38..7a57293 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -22,8 +22,8 @@
     android:fontFamily="@font/clock"
     android:includeFontPadding="false"
     android:textColor="@android:color/white"
-    android:format12Hour="h:mm"
-    android:format24Hour="kk:mm"
+    android:format12Hour="@string/dream_time_complication_12_hr_time_format"
+    android:format24Hour="@string/dream_time_complication_24_hr_time_format"
     android:shadowColor="@color/keyguard_shadow_color"
     android:shadowRadius="?attr/shadowRadius"
     android:textSize="@dimen/dream_overlay_complication_clock_time_text_size"/>
diff --git a/packages/SystemUI/res/layout/hybrid_conversation_notification.xml b/packages/SystemUI/res/layout/hybrid_conversation_notification.xml
index 43b1661..a313833 100644
--- a/packages/SystemUI/res/layout/hybrid_conversation_notification.xml
+++ b/packages/SystemUI/res/layout/hybrid_conversation_notification.xml
@@ -57,7 +57,8 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:singleLine="true"
-        style="?attr/hybridNotificationTextStyle"
+        android:paddingEnd="4dp"
+        style="@*android:style/Widget.DeviceDefault.Notification.Text"
     />
 
     <TextView
@@ -65,6 +66,7 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:singleLine="true"
-        style="?attr/hybridNotificationTextStyle"
+        android:paddingEnd="4dp"
+        style="@*android:style/Widget.DeviceDefault.Notification.Text"
     />
 </com.android.systemui.statusbar.notification.row.HybridConversationNotificationView>
diff --git a/packages/SystemUI/res/layout/hybrid_notification.xml b/packages/SystemUI/res/layout/hybrid_notification.xml
index e8d7751..9ea7be5 100644
--- a/packages/SystemUI/res/layout/hybrid_notification.xml
+++ b/packages/SystemUI/res/layout/hybrid_notification.xml
@@ -20,19 +20,22 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:gravity="bottom|start"
-    style="?attr/hybridNotificationStyle">
+    android:paddingStart="@*android:dimen/notification_content_margin_start"
+    android:paddingEnd="12dp">
     <TextView
         android:id="@+id/notification_title"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:singleLine="true"
-        style="?attr/hybridNotificationTitleStyle"
+        android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Notification.Title"
+        android:paddingEnd="4dp"
     />
     <TextView
         android:id="@+id/notification_text"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:singleLine="true"
-        style="?attr/hybridNotificationTextStyle"
+        android:paddingEnd="4dp"
+        style="@*android:style/Widget.DeviceDefault.Notification.Text"
     />
 </com.android.systemui.statusbar.notification.row.HybridNotificationView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml
index 4d24140..d886806 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip.xml
@@ -31,6 +31,8 @@
         android:padding="@dimen/media_ttt_chip_outer_padding"
         android:background="@drawable/media_ttt_chip_background"
         android:layout_marginTop="20dp"
+        android:layout_marginStart="@dimen/notification_side_paddings"
+        android:layout_marginEnd="@dimen/notification_side_paddings"
         android:clipToPadding="false"
         android:gravity="center_vertical"
         android:alpha="0.0"
@@ -46,8 +48,9 @@
 
         <TextView
             android:id="@+id/text"
-            android:layout_width="wrap_content"
+            android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_weight="1"
             android:textSize="@dimen/media_ttt_text_size"
             android:textColor="?android:attr/textColorPrimary"
             android:alpha="0.0"
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index 5e8b892..2b3d11b 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -14,20 +14,19 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<!-- TODO(b/203800646): layout_marginTop doesn't seem to work on some large screens. -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/media_ttt_receiver_chip"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:background="@drawable/media_ttt_chip_background_receiver"
     >
 
     <com.android.internal.widget.CachingIconView
         android:id="@+id/app_icon"
         android:layout_width="@dimen/media_ttt_icon_size_receiver"
         android:layout_height="@dimen/media_ttt_icon_size_receiver"
-        android:layout_gravity="center"
+        android:layout_gravity="center|bottom"
+        android:alpha="0.0"
         />
 
 </FrameLayout>
diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml
index e6af6f4..94fe209 100644
--- a/packages/SystemUI/res/values-h800dp/dimens.xml
+++ b/packages/SystemUI/res/values-h800dp/dimens.xml
@@ -15,9 +15,6 @@
   -->
 
 <resources>
-    <!-- Minimum margin between clock and top of screen or ambient indication -->
-    <dimen name="keyguard_clock_top_margin">26dp</dimen>
-
     <!-- Large clock maximum font size (dp is intentional, to prevent any further scaling) -->
     <dimen name="large_clock_text_size">200dp</dimen>
 
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 70a72ad..9a71995 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -108,12 +108,6 @@
         <attr name="android:layout" />
     </declare-styleable>
 
-    <declare-styleable name="HybridNotificationTheme">
-        <attr name="hybridNotificationStyle" format="reference" />
-        <attr name="hybridNotificationTitleStyle" format="reference" />
-        <attr name="hybridNotificationTextStyle" format="reference" />
-    </declare-styleable>
-
     <declare-styleable name="PluginInflateContainer">
         <attr name="viewType" format="string" />
     </declare-styleable>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 4d36541..36a2d64 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1060,6 +1060,7 @@
     <!-- Since the generic icon isn't circular, we need to scale it down so it still fits within
          the circular chip. -->
     <dimen name="media_ttt_generic_icon_size_receiver">70dp</dimen>
+    <dimen name="media_ttt_receiver_vert_translation">20dp</dimen>
 
     <!-- Window magnification -->
     <dimen name="magnification_border_drag_size">35dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d88428f..343ec4f6 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2581,4 +2581,12 @@
     <!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown -->
     <string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string>
 
+    <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] -->
+    <string name="dream_date_complication_date_format">EEE, MMM d</string>
+
+    <!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] -->
+    <string name="dream_time_complication_12_hr_time_format">h:mm</string>
+
+    <!-- Time format for the Dream Time Complication for 24-hour time format [CHAR LIMIT=NONE] -->
+    <string name="dream_time_complication_24_hr_time_format">kk:mm</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 8b2481c..f954bc9 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -17,30 +17,6 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
 
-    <!-- HybridNotification themes and styles -->
-
-    <style name="HybridNotification">
-        <item name="hybridNotificationStyle">@style/hybrid_notification</item>
-        <item name="hybridNotificationTitleStyle">@style/hybrid_notification_title</item>
-        <item name="hybridNotificationTextStyle">@style/hybrid_notification_text</item>
-    </style>
-
-    <style name="hybrid_notification">
-        <item name="android:paddingStart">@*android:dimen/notification_content_margin_start</item>
-        <item name="android:paddingEnd">12dp</item>
-    </style>
-
-    <style name="hybrid_notification_title">
-        <item name="android:paddingEnd">4dp</item>
-        <item name="android:textAppearance">@*android:style/TextAppearance.DeviceDefault.Notification.Title</item>
-    </style>
-
-    <style name="hybrid_notification_text"
-           parent="@*android:style/Widget.DeviceDefault.Notification.Text">
-        <item name="android:paddingEnd">4dp</item>
-    </style>
-
-
     <style name="TextAppearance.StatusBar.Clock" parent="@*android:style/TextAppearance.StatusBar.Icon">
         <item name="android:textSize">@dimen/status_bar_clock_size</item>
         <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp
new file mode 100644
index 0000000..a79fd9040d
--- /dev/null
+++ b/packages/SystemUI/screenshot/Android.bp
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+    name: "SystemUIScreenshotLib",
+    manifest: "AndroidManifest.xml",
+
+    srcs: [
+        // All files in this library should be in Kotlin besides some exceptions.
+        "src/**/*.kt",
+
+        // This file was forked from google3, so exceptionally it can be in Java.
+        "src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java",
+    ],
+
+    resource_dirs: [
+        "res",
+    ],
+
+    static_libs: [
+        "SystemUI-core",
+        "androidx.test.espresso.core",
+        "androidx.appcompat_appcompat",
+        "platform-screenshot-diff-core",
+    ],
+
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml
new file mode 100644
index 0000000..3b703be
--- /dev/null
+++ b/packages/SystemUI/screenshot/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.systemui.testing.screenshot">
+    <application>
+        <activity
+            android:name="com.android.systemui.testing.screenshot.ScreenshotActivity"
+            android:exported="true"
+            android:theme="@style/Theme.SystemUI.Screenshot" />
+    </application>
+
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+</manifest>
diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml
new file mode 100644
index 0000000..40e50bb
--- /dev/null
+++ b/packages/SystemUI/screenshot/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+
+        <!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java
new file mode 100644
index 0000000..96ec4c5
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.testing.screenshot;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+
+import org.json.JSONObject;
+import org.junit.function.ThrowingRunnable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+ * Note: This file was forked from
+ * google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/
+ * support/design/scuba/color/DynamicColorsTestUtils.java.
+ */
+
+/** Utility that helps change the dynamic system colors for testing. */
+@RequiresApi(32)
+public class DynamicColorsTestUtils {
+
+    private static final String TAG = DynamicColorsTestUtils.class.getSimpleName();
+
+    private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages";
+    private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY =
+            "android.theme.customization.system_palette";
+
+    private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800;
+    private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756;
+
+    private DynamicColorsTestUtils() {
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange
+     * seed color, and then wait for the change to propagate to the app by comparing
+     * android.R.color.system_accent1_600 to the expected orange value.
+     */
+    public static void updateSystemColorsToOrange() {
+        updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR);
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
+     * {@code seedColor}, and then wait for the change to propagate to the app by comparing
+     * android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}.
+     */
+    public static void updateSystemColors(
+            @ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) {
+        Context context = getInstrumentation().getTargetContext();
+
+        int actualSystemAccent1600 =
+                ContextCompat.getColor(context, android.R.color.system_accent1_600);
+
+        if (expectedSystemAccent1600 == actualSystemAccent1600) {
+            String expectedColorString = Integer.toHexString(expectedSystemAccent1600);
+            Log.d(
+                    TAG,
+                    "Skipped updating system colors since system_accent1_600 is already equal to "
+                            + "expected: "
+                            + expectedColorString);
+            return;
+        }
+
+        updateSystemColors(seedColor);
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
+     * {@code seedColor}, and then wait for the change to propagate to the app by checking
+     * android.R.color.system_accent1_600 for any change.
+     */
+    public static void updateSystemColors(@ColorInt int seedColor) {
+        Context context = getInstrumentation().getTargetContext();
+
+        // Initialize system color idling resource with original system_accent1_600 value.
+        ColorChangeIdlingResource systemColorIdlingResource =
+                new ColorChangeIdlingResource(context, android.R.color.system_accent1_600);
+
+        // Update system theme color setting to trigger fabricated resource overlay.
+        runWithShellPermissionIdentity(
+                () ->
+                        Settings.Secure.putString(
+                                context.getContentResolver(),
+                                THEME_CUSTOMIZATION_KEY,
+                                buildThemeCustomizationString(seedColor)));
+
+        // Wait for system color update to propagate to app.
+        IdlingRegistry idlingRegistry = IdlingRegistry.getInstance();
+        idlingRegistry.register(systemColorIdlingResource);
+        Espresso.onIdle();
+        idlingRegistry.unregister(systemColorIdlingResource);
+
+        Log.d(TAG,
+                Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY));
+    }
+
+    private static String buildThemeCustomizationString(@ColorInt int seedColor) {
+        String seedColorHex = Integer.toHexString(seedColor);
+        Map<String, String> themeCustomizationMap = new HashMap<>();
+        themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex);
+        return new JSONObject(themeCustomizationMap).toString();
+    }
+
+    private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) {
+        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    private static class ColorChangeIdlingResource implements IdlingResource {
+
+        private final Context mContext;
+        private final int mColorResId;
+        private final int mInitialColorInt;
+
+        private ResourceCallback mResourceCallback;
+        private boolean mIdleNow;
+
+        ColorChangeIdlingResource(Context context, @ColorRes int colorResId) {
+            this.mContext = context;
+            this.mColorResId = colorResId;
+            this.mInitialColorInt = ContextCompat.getColor(context, colorResId);
+        }
+
+        @Override
+        public String getName() {
+            return ColorChangeIdlingResource.class.getName();
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            if (mIdleNow) {
+                return true;
+            }
+
+            int currentColorInt = ContextCompat.getColor(mContext, mColorResId);
+
+            String initialColorString = Integer.toHexString(mInitialColorInt);
+            String currentColorString = Integer.toHexString(currentColorInt);
+            Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString,
+                    currentColorString));
+
+            mIdleNow = currentColorInt != mInitialColorInt;
+            Log.d(TAG, String.format("idleNow=%b", mIdleNow));
+
+            if (mIdleNow) {
+                mResourceCallback.onTransitionToIdle();
+            }
+            return mIdleNow;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+            this.mResourceCallback = resourceCallback;
+        }
+    }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt
new file mode 100644
index 0000000..2a55a80
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.testing.screenshot
+
+import androidx.activity.ComponentActivity
+
+/** The Activity that is launched and whose content is set for screenshot tests. */
+class ScreenshotActivity : ComponentActivity()
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt
new file mode 100644
index 0000000..363ce10
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.testing.screenshot
+
+import android.app.UiModeManager
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.os.UserHandle
+import android.view.Display
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.GoldenImagePathManager
+import platform.test.screenshot.PathConfig
+import platform.test.screenshot.PathElementNoContext
+import platform.test.screenshot.ScreenshotTestRule
+import platform.test.screenshot.matchers.PixelPerfectMatcher
+
+/**
+ * A base rule for screenshot diff tests.
+ *
+ * This rules takes care of setting up the activity according to [testSpec] by:
+ * - emulating the display size and density.
+ * - setting the dark/light mode.
+ * - setting the system (Material You) colors to a fixed value.
+ *
+ * @see ComposeScreenshotTestRule
+ * @see ViewScreenshotTestRule
+ */
+class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule {
+    private var currentDisplay: DisplaySpec? = null
+    private var currentGoldenIdentifier: String? = null
+
+    private val pathConfig =
+        PathConfig(
+            PathElementNoContext("model", isDir = true) {
+                currentDisplay?.name ?: error("currentDisplay is null")
+            },
+        )
+    private val defaultMatcher = PixelPerfectMatcher()
+
+    private val screenshotRule =
+        ScreenshotTestRule(
+            SystemUIGoldenImagePathManager(
+                pathConfig,
+                currentGoldenIdentifier = {
+                    currentGoldenIdentifier ?: error("currentGoldenIdentifier is null")
+                },
+            )
+        )
+
+    override fun apply(base: Statement, description: Description): Statement {
+        // The statement which call beforeTest() before running the test and afterTest() afterwards.
+        val statement =
+            object : Statement() {
+                override fun evaluate() {
+                    try {
+                        beforeTest()
+                        base.evaluate()
+                    } finally {
+                        afterTest()
+                    }
+                }
+            }
+
+        return screenshotRule.apply(statement, description)
+    }
+
+    private fun beforeTest() {
+        // Update the system colors to a fixed color, so that tests don't depend on the host device
+        // extracted colors. Note that we don't restore the default device colors at the end of the
+        // test because changing the colors (and waiting for them to be applied) is costly and makes
+        // the screenshot tests noticeably slower.
+        DynamicColorsTestUtils.updateSystemColorsToOrange()
+
+        // Emulate the display size and density.
+        val display = testSpec.display
+        val density = display.densityDpi
+        val wm = WindowManagerGlobal.getWindowManagerService()
+        val (width, height) = getEmulatedDisplaySize()
+        wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId())
+        wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height)
+
+        // Force the dark/light theme.
+        val uiModeManager =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+        uiModeManager.setApplicationNightMode(
+            if (testSpec.isDarkTheme) {
+                UiModeManager.MODE_NIGHT_YES
+            } else {
+                UiModeManager.MODE_NIGHT_NO
+            }
+        )
+    }
+
+    private fun afterTest() {
+        // Reset the density and display size.
+        val wm = WindowManagerGlobal.getWindowManagerService()
+        wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId())
+        wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY)
+
+        // Reset the dark/light theme.
+        val uiModeManager =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+        uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
+    }
+
+    /**
+     * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
+     * context of [testSpec].
+     */
+    fun screenshotTest(goldenIdentifier: String, view: View) {
+        val bitmap = drawIntoBitmap(view)
+
+        // Compare bitmap against golden asset.
+        val isDarkTheme = testSpec.isDarkTheme
+        val isLandscape = testSpec.isLandscape
+        val identifierWithSpec = buildString {
+            append(goldenIdentifier)
+            if (isDarkTheme) append("_dark")
+            if (isLandscape) append("_landscape")
+        }
+
+        // TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on
+        // device to assertBitmapAgainstGolden instead?
+        currentDisplay = testSpec.display
+        currentGoldenIdentifier = goldenIdentifier
+        screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher)
+        currentDisplay = null
+        currentGoldenIdentifier = goldenIdentifier
+    }
+
+    /** Draw [view] into a [Bitmap]. */
+    private fun drawIntoBitmap(view: View): Bitmap {
+        val bitmap =
+            Bitmap.createBitmap(
+                view.measuredWidth,
+                view.measuredHeight,
+                Bitmap.Config.ARGB_8888,
+            )
+        val canvas = Canvas(bitmap)
+        view.draw(canvas)
+        return bitmap
+    }
+
+    /** Get the emulated display size for [testSpec]. */
+    private fun getEmulatedDisplaySize(): Pair<Int, Int> {
+        val display = testSpec.display
+        val isPortraitNaturalPosition = display.width < display.height
+        return if (testSpec.isLandscape) {
+            if (isPortraitNaturalPosition) {
+                display.height to display.width
+            } else {
+                display.width to display.height
+            }
+        } else {
+            if (isPortraitNaturalPosition) {
+                display.width to display.height
+            } else {
+                display.height to display.width
+            }
+        }
+    }
+}
+
+private class SystemUIGoldenImagePathManager(
+    pathConfig: PathConfig,
+    private val currentGoldenIdentifier: () -> String,
+) :
+    GoldenImagePathManager(
+        appContext = InstrumentationRegistry.getInstrumentation().context,
+        deviceLocalPath =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .filesDir
+                .absolutePath
+                .toString() + "/sysui_screenshots",
+        pathConfig = pathConfig,
+    ) {
+    // This string is appended to all actual/expected screenshots on the device. We append the
+    // golden identifier so that our pull_golden.py scripts can map a screenshot on device to its
+    // asset (and automatically update it, if necessary).
+    override fun toString() = currentGoldenIdentifier()
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt
new file mode 100644
index 0000000..7fc6245
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.testing.screenshot
+
+/** The specification of a device display to be used in a screenshot test. */
+data class DisplaySpec(
+    val name: String,
+    val width: Int,
+    val height: Int,
+    val densityDpi: Int,
+)
+
+/** The specification of a screenshot diff test. */
+class ScreenshotTestSpec(
+    val display: DisplaySpec,
+    val isDarkTheme: Boolean = false,
+    val isLandscape: Boolean = false,
+) {
+    companion object {
+        /**
+         * Return a list of [ScreenshotTestSpec] for each of the [displays].
+         *
+         * If [isDarkTheme] is null, this will create a spec for both light and dark themes, for
+         * each of the orientation.
+         *
+         * If [isLandscape] is null, this will create a spec for both portrait and landscape, for
+         * each of the light/dark themes.
+         */
+        fun forDisplays(
+            vararg displays: DisplaySpec,
+            isDarkTheme: Boolean? = null,
+            isLandscape: Boolean? = null,
+        ): List<ScreenshotTestSpec> {
+            return displays.flatMap { display ->
+                buildList {
+                    fun addDisplay(isLandscape: Boolean) {
+                        if (isDarkTheme != true) {
+                            add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape))
+                        }
+
+                        if (isDarkTheme != false) {
+                            add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape))
+                        }
+                    }
+
+                    if (isLandscape != true) {
+                        addDisplay(isLandscape = false)
+                    }
+
+                    if (isLandscape != false) {
+                        addDisplay(isLandscape = true)
+                    }
+                }
+            }
+        }
+    }
+
+    override fun toString(): String = buildString {
+        // This string is appended to PNGs stored in the device, so let's keep it simple.
+        append(display.name)
+        if (isDarkTheme) append("_dark")
+        if (isLandscape) append("_landscape")
+    }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
new file mode 100644
index 0000000..35812e3
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -0,0 +1,80 @@
+package com.android.systemui.testing.screenshot
+
+import android.app.Activity
+import android.app.Dialog
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertEquals
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** A rule for View screenshot diff tests. */
+class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule {
+    private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
+    private val screenshotRule = ScreenshotTestRule(testSpec)
+
+    private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule)
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return delegate.apply(base, description)
+    }
+
+    /**
+     * Compare the content of the view provided by [viewProvider] with the golden image identified
+     * by [goldenIdentifier] in the context of [testSpec].
+     */
+    fun screenshotTest(
+        goldenIdentifier: String,
+        layoutParams: LayoutParams =
+            LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
+        viewProvider: (Activity) -> View,
+    ) {
+        activityRule.scenario.onActivity { activity ->
+            // Make sure that the activity draws full screen and fits the whole display instead of
+            // the system bars.
+            activity.window.setDecorFitsSystemWindows(false)
+            activity.setContentView(viewProvider(activity), layoutParams)
+        }
+
+        // We call onActivity again because it will make sure that our Activity is done measuring,
+        // laying out and drawing its content (that we set in the previous onActivity lambda).
+        activityRule.scenario.onActivity { activity ->
+            // Check that the content is what we expected.
+            val content = activity.requireViewById<ViewGroup>(android.R.id.content)
+            assertEquals(1, content.childCount)
+            screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0))
+        }
+    }
+
+    /**
+     * Compare the content of the dialog provided by [dialogProvider] with the golden image
+     * identified by [goldenIdentifier] in the context of [testSpec].
+     */
+    fun dialogScreenshotTest(
+        goldenIdentifier: String,
+        dialogProvider: (Activity) -> Dialog,
+    ) {
+        var dialog: Dialog? = null
+        activityRule.scenario.onActivity { activity ->
+            // Make sure that the dialog draws full screen and fits the whole display instead of the
+            // system bars.
+            dialog =
+                dialogProvider(activity).apply {
+                    window.setDecorFitsSystemWindows(false)
+                    show()
+                }
+        }
+
+        // We call onActivity again because it will make sure that our Dialog is done measuring,
+        // laying out and drawing its content (that we set in the previous onActivity lambda).
+        activityRule.scenario.onActivity {
+            // Check that the content is what we expected.
+            val dialog = dialog ?: error("dialog is null")
+            screenshotRule.screenshotTest(goldenIdentifier, dialog.window.decorView)
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
index 88fe034..203b236 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
@@ -80,7 +80,8 @@
 
     public PictureInPictureSurfaceTransaction scaleAndCrop(
             SurfaceControl.Transaction tx, SurfaceControl leash,
-            Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets) {
+            Rect sourceRectHint, Rect sourceBounds, Rect destinationBounds, Rect insets,
+            float progress) {
         mTmpSourceRectF.set(sourceBounds);
         mTmpDestinationRect.set(sourceBounds);
         mTmpDestinationRect.inset(insets);
@@ -93,9 +94,13 @@
                     : (float) destinationBounds.height() / sourceBounds.height();
         } else {
             // scale by sourceRectHint if it's not edge-to-edge
-            scale = sourceRectHint.width() <= sourceRectHint.height()
+            final float endScale = sourceRectHint.width() <= sourceRectHint.height()
                     ? (float) destinationBounds.width() / sourceRectHint.width()
                     : (float) destinationBounds.height() / sourceRectHint.height();
+            final float startScale = sourceRectHint.width() <= sourceRectHint.height()
+                    ? (float) destinationBounds.width() / sourceBounds.width()
+                    : (float) destinationBounds.height() / sourceBounds.height();
+            scale = (1 - progress) * startScale + progress * endScale;
         }
         final float left = destinationBounds.left - (insets.left + sourceBounds.left) * scale;
         final float top = destinationBounds.top - (insets.top + sourceBounds.top) * scale;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 76a09b3..9265f07 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -105,7 +105,7 @@
             }
 
             @Override
-            public void onAnimationCancelled() {
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 remoteAnimationAdapter.onAnimationCancelled();
             }
         };
@@ -114,6 +114,8 @@
     private static IRemoteTransition.Stub wrapRemoteTransition(
             final RemoteAnimationRunnerCompat remoteAnimationAdapter) {
         return new IRemoteTransition.Stub() {
+            final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>();
+
             @Override
             public void startAnimation(IBinder token, TransitionInfo info,
                     SurfaceControl.Transaction t,
@@ -218,9 +220,9 @@
                         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                             info.getChanges().get(i).getLeash().release();
                         }
-                        for (int i = leashMap.size() - 1; i >= 0; --i) {
-                            leashMap.valueAt(i).release();
-                        }
+                        // Don't release here since launcher might still be using them. Instead
+                        // let launcher release them (eg. via RemoteAnimationTargets)
+                        leashMap.clear();
                         try {
                             finishCallback.onTransitionFinished(null /* wct */, finishTransaction);
                         } catch (RemoteException e) {
@@ -229,19 +231,32 @@
                         }
                     }
                 };
+                synchronized (mFinishRunnables) {
+                    mFinishRunnables.put(token, animationFinishedCallback);
+                }
                 // TODO(bc-unlcok): Pass correct transit type.
-                remoteAnimationAdapter.onAnimationStart(
-                        TRANSIT_OLD_NONE,
-                        appsCompat, wallpapersCompat, nonAppsCompat,
-                        animationFinishedCallback);
+                remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE,
+                        appsCompat, wallpapersCompat, nonAppsCompat, () -> {
+                            synchronized (mFinishRunnables) {
+                                if (mFinishRunnables.remove(token) == null) return;
+                            }
+                            animationFinishedCallback.run();
+                        });
             }
 
             @Override
             public void mergeAnimation(IBinder token, TransitionInfo info,
                     SurfaceControl.Transaction t, IBinder mergeTarget,
                     IRemoteTransitionFinishedCallback finishCallback) {
-                // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, ignore
-                //       any incoming merges.
+                // TODO: hook up merge to recents onTaskAppeared if applicable. Until then, adapt
+                //       to legacy cancel.
+                final Runnable finishRunnable;
+                synchronized (mFinishRunnables) {
+                    finishRunnable = mFinishRunnables.remove(mergeTarget);
+                }
+                if (finishRunnable == null) return;
+                remoteAnimationAdapter.onAnimationCancelled();
+                finishRunnable.run();
             }
         };
     }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 4ce110b..249133a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -141,8 +141,10 @@
         final int mode = change.getMode();
 
         t.reparent(leash, info.getRootLeash());
-        t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
-                change.getStartAbsBounds().top - info.getRootOffset().y);
+        final Rect absBounds =
+                (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
+        t.setPosition(leash, absBounds.left - info.getRootOffset().x,
+                absBounds.top - info.getRootOffset().y);
 
         // Put all the OPEN/SHOW on top
         if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
index 83780c8..29e912f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java
@@ -59,6 +59,7 @@
     private final boolean mShowImeAtScreenOn;
     private EditText mPasswordEntry;
     private ImageView mSwitchImeButton;
+    private boolean mPaused;
 
     private final OnEditorActionListener mOnEditorActionListener = (v, actionId, event) -> {
         // Check if this was the result of hitting the enter key
@@ -202,6 +203,7 @@
     @Override
     public void onResume(int reason) {
         super.onResume(reason);
+        mPaused = false;
         if (reason != KeyguardSecurityView.SCREEN_ON || mShowImeAtScreenOn) {
             showInput();
         }
@@ -223,6 +225,11 @@
 
     @Override
     public void onPause() {
+        if (mPaused) {
+            return;
+        }
+        mPaused = true;
+
         if (!mPasswordEntry.isVisibleToUser()) {
             // Reset all states directly and then hide IME when the screen turned off.
             super.onPause();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 65c415b..8fb622a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -94,6 +94,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class KeyguardSecurityContainer extends FrameLayout {
     static final int USER_TYPE_PRIMARY = 1;
@@ -128,12 +129,12 @@
 
     private static final long IME_DISAPPEAR_DURATION_MS = 125;
 
-    // The duration of the animation to switch bouncer sides.
-    private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500;
+    // The duration of the animation to switch security sides.
+    private static final long SECURITY_SHIFT_ANIMATION_DURATION_MS = 500;
 
-    // How much of the switch sides animation should be dedicated to fading the bouncer out. The
+    // How much of the switch sides animation should be dedicated to fading the security out. The
     // remainder will fade it back in again.
-    private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
+    private static final float SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION = 0.2f;
 
     @VisibleForTesting
     KeyguardSecurityViewFlipper mSecurityViewFlipper;
@@ -375,10 +376,23 @@
         mViewMode.updatePositionByTouchX(x);
     }
 
-    /** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */
-    public boolean isOneHandedModeLeftAligned() {
-        return mCurrentMode == MODE_ONE_HANDED
-                && ((OneHandedViewMode) mViewMode).isLeftAligned();
+    public boolean isSidedSecurityMode() {
+        return mViewMode instanceof SidedSecurityMode;
+    }
+
+    /** Returns whether the inner SecurityViewFlipper is left-aligned when in sided mode. */
+    public boolean isSecurityLeftAligned() {
+        return mViewMode instanceof SidedSecurityMode
+                && ((SidedSecurityMode) mViewMode).isLeftAligned();
+    }
+
+    /**
+     * Returns whether the touch happened on the other side of security (like bouncer) when in
+     * sided mode.
+     */
+    public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) {
+        return mViewMode instanceof SidedSecurityMode
+                && ((SidedSecurityMode) mViewMode).isTouchOnTheOtherSideOfSecurity(ev);
     }
 
     public void onPause() {
@@ -437,12 +451,15 @@
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         final int action = event.getActionMasked();
-        mDoubleTapDetector.onTouchEvent(event);
 
         boolean result =  mMotionEventListeners.stream()
                 .anyMatch(listener -> listener.onTouchEvent(event))
                 || super.onTouchEvent(event);
 
+        // double tap detector should be called after listeners handle touches as listeners are
+        // helping with ignoring falsing. Otherwise falsing will be activated for some double taps
+        mDoubleTapDetector.onTouchEvent(event);
+
         switch (action) {
             case MotionEvent.ACTION_MOVE:
                 mVelocityTracker.addMovement(event);
@@ -487,11 +504,16 @@
     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
         @Override
         public boolean onDoubleTap(MotionEvent e) {
-            if (!mIsDragging) {
-                mViewMode.handleDoubleTap(e);
-            }
+            return handleDoubleTap(e);
+        }
+    }
+
+    @VisibleForTesting boolean handleDoubleTap(MotionEvent e) {
+        if (!mIsDragging) {
+            mViewMode.handleDoubleTap(e);
             return true;
         }
+        return false;
     }
 
     void addMotionEventListener(Gefingerpoken listener) {
@@ -771,6 +793,195 @@
     }
 
     /**
+     * Base class for modes which support having on left/right side of the screen, used for large
+     * screen devices
+     */
+    abstract static class SidedSecurityMode implements ViewMode {
+        @Nullable private ValueAnimator mRunningSecurityShiftAnimator;
+        private KeyguardSecurityViewFlipper mViewFlipper;
+        private ViewGroup mView;
+        private GlobalSettings mGlobalSettings;
+        private int mDefaultSideSetting;
+
+        public void init(ViewGroup v, KeyguardSecurityViewFlipper viewFlipper,
+                GlobalSettings globalSettings, boolean leftAlignedByDefault) {
+            mView = v;
+            mViewFlipper = viewFlipper;
+            mGlobalSettings = globalSettings;
+            mDefaultSideSetting =
+                    leftAlignedByDefault ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
+                            : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT;
+        }
+
+        /**
+         * Determine if a double tap on this view is on the other side. If so, will animate
+         * positions and record the preference to always show on this side.
+         */
+        @Override
+        public void handleDoubleTap(MotionEvent event) {
+            boolean currentlyLeftAligned = isLeftAligned();
+            // Did the tap hit the "other" side of the bouncer?
+            if (isTouchOnTheOtherSideOfSecurity(event, currentlyLeftAligned)) {
+                boolean willBeLeftAligned = !currentlyLeftAligned;
+                updateSideSetting(willBeLeftAligned);
+
+                int keyguardState = willBeLeftAligned
+                        ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT
+                        : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT;
+                SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState);
+
+                updateSecurityViewLocation(willBeLeftAligned, /* animate= */ true);
+            }
+        }
+
+        private boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev, boolean leftAligned) {
+            float x = ev.getX();
+            return (leftAligned && (x > mView.getWidth() / 2f))
+                    || (!leftAligned && (x < mView.getWidth() / 2f));
+        }
+
+        public boolean isTouchOnTheOtherSideOfSecurity(MotionEvent ev) {
+            return isTouchOnTheOtherSideOfSecurity(ev, isLeftAligned());
+        }
+
+        protected abstract void updateSecurityViewLocation(boolean leftAlign, boolean animate);
+
+        protected void translateSecurityViewLocation(boolean leftAlign, boolean animate) {
+            translateSecurityViewLocation(leftAlign, animate, i -> {});
+        }
+
+        /**
+         * Moves the inner security view to the correct location with animation. This is triggered
+         * when the user double taps on the side of the screen that is not currently occupied by
+         * the security view.
+         */
+        protected void translateSecurityViewLocation(boolean leftAlign, boolean animate,
+                Consumer<Float> securityAlphaListener) {
+            if (mRunningSecurityShiftAnimator != null) {
+                mRunningSecurityShiftAnimator.cancel();
+                mRunningSecurityShiftAnimator = null;
+            }
+
+            int targetTranslation = leftAlign
+                    ? 0 : mView.getMeasuredWidth() - mViewFlipper.getWidth();
+
+            if (animate) {
+                // This animation is a bit fun to implement. The bouncer needs to move, and fade
+                // in/out at the same time. The issue is, the bouncer should only move a short
+                // amount (120dp or so), but obviously needs to go from one side of the screen to
+                // the other. This needs a pretty custom animation.
+                //
+                // This works as follows. It uses a ValueAnimation to simply drive the animation
+                // progress. This animator is responsible for both the translation of the bouncer,
+                // and the current fade. It will fade the bouncer out while also moving it along the
+                // 120dp path. Once the bouncer is fully faded out though, it will "snap" the
+                // bouncer closer to its destination, then fade it back in again. The effect is that
+                // the bouncer will move from 0 -> X while fading out, then
+                // (destination - X) -> destination while fading back in again.
+                // TODO(b/208250221): Make this animation properly abortable.
+                Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
+                        mView.getContext(), android.R.interpolator.fast_out_extra_slow_in);
+                Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
+                Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
+
+                mRunningSecurityShiftAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+                mRunningSecurityShiftAnimator.setDuration(SECURITY_SHIFT_ANIMATION_DURATION_MS);
+                mRunningSecurityShiftAnimator.setInterpolator(Interpolators.LINEAR);
+
+                int initialTranslation = (int) mViewFlipper.getTranslationX();
+                int totalTranslation = (int) mView.getResources().getDimension(
+                        R.dimen.security_shift_animation_translation);
+
+                final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering()
+                        && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
+                if (shouldRestoreLayerType) {
+                    mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
+                }
+
+                float initialAlpha = mViewFlipper.getAlpha();
+
+                mRunningSecurityShiftAnimator.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        mRunningSecurityShiftAnimator = null;
+                    }
+                });
+                mRunningSecurityShiftAnimator.addUpdateListener(animation -> {
+                    float switchPoint = SECURITY_SHIFT_ANIMATION_FADE_OUT_PROPORTION;
+                    boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
+
+                    int currentTranslation = (int) (positionInterpolator.getInterpolation(
+                            animation.getAnimatedFraction()) * totalTranslation);
+                    int translationRemaining = totalTranslation - currentTranslation;
+
+                    // Flip the sign if we're going from right to left.
+                    if (leftAlign) {
+                        currentTranslation = -currentTranslation;
+                        translationRemaining = -translationRemaining;
+                    }
+
+                    float opacity;
+                    if (isFadingOut) {
+                        // The bouncer fades out over the first X%.
+                        float fadeOutFraction = MathUtils.constrainedMap(
+                                /* rangeMin= */1.0f,
+                                /* rangeMax= */0.0f,
+                                /* valueMin= */0.0f,
+                                /* valueMax= */switchPoint,
+                                animation.getAnimatedFraction());
+                        opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
+
+                        // When fading out, the alpha needs to start from the initial opacity of the
+                        // view flipper, otherwise we get a weird bit of jank as it ramps back to
+                        // 100%.
+                        mViewFlipper.setAlpha(opacity * initialAlpha);
+
+                        // Animate away from the source.
+                        mViewFlipper.setTranslationX(initialTranslation + currentTranslation);
+                    } else {
+                        // And in again over the remaining (100-X)%.
+                        float fadeInFraction = MathUtils.constrainedMap(
+                                /* rangeMin= */0.0f,
+                                /* rangeMax= */1.0f,
+                                /* valueMin= */switchPoint,
+                                /* valueMax= */1.0f,
+                                animation.getAnimatedFraction());
+
+                        opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
+                        mViewFlipper.setAlpha(opacity);
+
+                        // Fading back in, animate towards the destination.
+                        mViewFlipper.setTranslationX(targetTranslation - translationRemaining);
+                    }
+                    securityAlphaListener.accept(opacity);
+
+                    if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
+                        mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
+                    }
+                });
+
+                mRunningSecurityShiftAnimator.start();
+            } else {
+                mViewFlipper.setTranslationX(targetTranslation);
+            }
+        }
+
+
+        boolean isLeftAligned() {
+            return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
+                    mDefaultSideSetting)
+                    == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
+        }
+
+        protected void updateSideSetting(boolean leftAligned) {
+            mGlobalSettings.putInt(
+                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
+                    leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
+                            : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
+        }
+    }
+
+    /**
      * Default bouncer is centered within the space
      */
     static class DefaultViewMode implements ViewMode {
@@ -802,7 +1013,7 @@
      * User switcher mode will display both the current user icon as well as
      * a user switcher, in both portrait and landscape modes.
      */
-    static class UserSwitcherViewMode implements ViewMode {
+    static class UserSwitcherViewMode extends SidedSecurityMode {
         private ViewGroup mView;
         private ViewGroup mUserSwitcherViewGroup;
         private KeyguardSecurityViewFlipper mViewFlipper;
@@ -814,11 +1025,15 @@
         private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
                 this::setupUserSwitcher;
 
+        private float mAnimationLastAlpha = 1f;
+        private boolean mAnimationWaitsToShift = true;
+
         @Override
         public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
                 @NonNull UserSwitcherController userSwitcherController) {
+            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */false);
             mView = v;
             mViewFlipper = viewFlipper;
             mFalsingManager = falsingManager;
@@ -832,9 +1047,7 @@
                         true);
                 mUserSwitcherViewGroup =  mView.findViewById(R.id.keyguard_bouncer_user_switcher);
             }
-
             updateSecurityViewLocation();
-
             mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
             setupUserSwitcher();
             mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
@@ -1030,18 +1243,65 @@
 
         @Override
         public void updateSecurityViewLocation() {
-            int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
+        }
 
+        public void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
+            setYTranslation();
+            setGravity();
+            setXTranslation(leftAlign, animate);
+        }
+
+        private void setXTranslation(boolean leftAlign, boolean animate) {
             if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
-                updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
-                updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
+                mUserSwitcherViewGroup.setTranslationX(0);
+                mViewFlipper.setTranslationX(0);
+            } else {
+                int switcherTargetTranslation = leftAlign
+                        ? mView.getMeasuredWidth() - mViewFlipper.getWidth() : 0;
+                if (animate) {
+                    mAnimationWaitsToShift = true;
+                    mAnimationLastAlpha = 1f;
+                    translateSecurityViewLocation(leftAlign, animate, securityAlpha -> {
+                        // During the animation security view fades out - alpha goes from 1 to
+                        // (almost) 0 - and then fades in - alpha grows back to 1.
+                        // If new alpha is bigger than previous one it means we're at inflection
+                        // point and alpha is zero or almost zero. That's when we want to do
+                        // translation of user switcher, so that it's not visible to the user.
+                        boolean fullyFadeOut = securityAlpha == 0.0f
+                                || securityAlpha > mAnimationLastAlpha;
+                        if (fullyFadeOut && mAnimationWaitsToShift) {
+                            mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation);
+                            mAnimationWaitsToShift = false;
+                        }
+                        mUserSwitcherViewGroup.setAlpha(securityAlpha);
+                        mAnimationLastAlpha = securityAlpha;
+                    });
+                } else {
+                    translateSecurityViewLocation(leftAlign, animate);
+                    mUserSwitcherViewGroup.setTranslationX(switcherTargetTranslation);
+                }
+            }
 
+        }
+
+        private void setGravity() {
+            if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
+                updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
+                updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
+            } else {
+                // horizontal gravity is the same because we translate these views anyway
+                updateViewGravity(mViewFlipper, Gravity.LEFT | Gravity.BOTTOM);
+                updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL);
+            }
+        }
+
+        private void setYTranslation() {
+            int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
+            if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
                 mUserSwitcherViewGroup.setTranslationY(yTrans);
                 mViewFlipper.setTranslationY(-yTrans);
             } else {
-                updateViewGravity(mViewFlipper, Gravity.RIGHT | Gravity.BOTTOM);
-                updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL);
-
                 // Attempt to reposition a bit higher to make up for this frame being a bit lower
                 // on the device
                 mUserSwitcherViewGroup.setTranslationY(-yTrans);
@@ -1060,20 +1320,18 @@
      * Logic to enabled one-handed bouncer mode. Supports animating the bouncer
      * between alternate sides of the display.
      */
-    static class OneHandedViewMode implements ViewMode {
-        @Nullable private ValueAnimator mRunningOneHandedAnimator;
+    static class OneHandedViewMode extends SidedSecurityMode {
         private ViewGroup mView;
         private KeyguardSecurityViewFlipper mViewFlipper;
-        private GlobalSettings mGlobalSettings;
 
         @Override
         public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
                 @NonNull KeyguardSecurityViewFlipper viewFlipper,
                 @NonNull FalsingManager falsingManager,
                 @NonNull UserSwitcherController userSwitcherController) {
+            init(v, viewFlipper, globalSettings, /* leftAlignedByDefault= */true);
             mView = v;
             mViewFlipper = viewFlipper;
-            mGlobalSettings = globalSettings;
 
             updateSecurityViewGravity();
             updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
@@ -1107,159 +1365,13 @@
             updateSecurityViewLocation(isTouchOnLeft, /* animate= */false);
         }
 
-        boolean isLeftAligned() {
-            return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
-                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT)
-                == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
-        }
-
-        private void updateSideSetting(boolean leftAligned) {
-            mGlobalSettings.putInt(
-                    Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
-                    leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
-                    : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
-        }
-
-        /**
-         * Determine if a double tap on this view is on the other side. If so, will animate
-         * positions and record the preference to always show on this side.
-         */
-        @Override
-        public void handleDoubleTap(MotionEvent event) {
-            float x = event.getX();
-            boolean currentlyLeftAligned = isLeftAligned();
-            // Did the tap hit the "other" side of the bouncer?
-            if ((currentlyLeftAligned && (x > mView.getWidth() / 2f))
-                    || (!currentlyLeftAligned && (x < mView.getWidth() / 2f))) {
-
-                boolean willBeLeftAligned = !currentlyLeftAligned;
-                updateSideSetting(willBeLeftAligned);
-
-                int keyguardState = willBeLeftAligned
-                        ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT
-                        : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT;
-                SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState);
-
-                updateSecurityViewLocation(willBeLeftAligned, true /* animate */);
-            }
-        }
-
         @Override
         public void updateSecurityViewLocation() {
             updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
         }
 
-        /**
-         * Moves the inner security view to the correct location (in one handed mode) with
-         * animation. This is triggered when the user taps on the side of the screen that is not
-         * currently occupied by the security view.
-         */
-        private void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
-            if (mRunningOneHandedAnimator != null) {
-                mRunningOneHandedAnimator.cancel();
-                mRunningOneHandedAnimator = null;
-            }
-
-            int targetTranslation = leftAlign
-                    ? 0 : (int) (mView.getMeasuredWidth() - mViewFlipper.getWidth());
-
-            if (animate) {
-                // This animation is a bit fun to implement. The bouncer needs to move, and fade
-                // in/out at the same time. The issue is, the bouncer should only move a short
-                // amount (120dp or so), but obviously needs to go from one side of the screen to
-                // the other. This needs a pretty custom animation.
-                //
-                // This works as follows. It uses a ValueAnimation to simply drive the animation
-                // progress. This animator is responsible for both the translation of the bouncer,
-                // and the current fade. It will fade the bouncer out while also moving it along the
-                // 120dp path. Once the bouncer is fully faded out though, it will "snap" the
-                // bouncer closer to its destination, then fade it back in again. The effect is that
-                // the bouncer will move from 0 -> X while fading out, then
-                // (destination - X) -> destination while fading back in again.
-                // TODO(b/208250221): Make this animation properly abortable.
-                Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
-                        mView.getContext(), android.R.interpolator.fast_out_extra_slow_in);
-                Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN;
-                Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
-
-                mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
-                mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS);
-                mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR);
-
-                int initialTranslation = (int) mViewFlipper.getTranslationX();
-                int totalTranslation = (int) mView.getResources().getDimension(
-                        R.dimen.one_handed_bouncer_move_animation_translation);
-
-                final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering()
-                        && mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
-                if (shouldRestoreLayerType) {
-                    mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
-                }
-
-                float initialAlpha = mViewFlipper.getAlpha();
-
-                mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() {
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            mRunningOneHandedAnimator = null;
-                        }
-                    });
-                mRunningOneHandedAnimator.addUpdateListener(animation -> {
-                    float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION;
-                    boolean isFadingOut = animation.getAnimatedFraction() < switchPoint;
-
-                    int currentTranslation = (int) (positionInterpolator.getInterpolation(
-                            animation.getAnimatedFraction()) * totalTranslation);
-                    int translationRemaining = totalTranslation - currentTranslation;
-
-                    // Flip the sign if we're going from right to left.
-                    if (leftAlign) {
-                        currentTranslation = -currentTranslation;
-                        translationRemaining = -translationRemaining;
-                    }
-
-                    if (isFadingOut) {
-                        // The bouncer fades out over the first X%.
-                        float fadeOutFraction = MathUtils.constrainedMap(
-                                /* rangeMin= */1.0f,
-                                /* rangeMax= */0.0f,
-                                /* valueMin= */0.0f,
-                                /* valueMax= */switchPoint,
-                                animation.getAnimatedFraction());
-                        float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction);
-
-                        // When fading out, the alpha needs to start from the initial opacity of the
-                        // view flipper, otherwise we get a weird bit of jank as it ramps back to
-                        // 100%.
-                        mViewFlipper.setAlpha(opacity * initialAlpha);
-
-                        // Animate away from the source.
-                        mViewFlipper.setTranslationX(initialTranslation + currentTranslation);
-                    } else {
-                        // And in again over the remaining (100-X)%.
-                        float fadeInFraction = MathUtils.constrainedMap(
-                                /* rangeMin= */0.0f,
-                                /* rangeMax= */1.0f,
-                                /* valueMin= */switchPoint,
-                                /* valueMax= */1.0f,
-                                animation.getAnimatedFraction());
-
-                        float opacity = fadeInInterpolator.getInterpolation(fadeInFraction);
-                        mViewFlipper.setAlpha(opacity);
-
-                        // Fading back in, animate towards the destination.
-                        mViewFlipper.setTranslationX(targetTranslation - translationRemaining);
-                    }
-
-                    if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
-                        mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
-                    }
-                });
-
-                mRunningOneHandedAnimator.start();
-            } else {
-                mViewFlipper.setTranslationX(targetTranslation);
-            }
+        protected void updateSecurityViewLocation(boolean leftAlign, boolean animate) {
+            translateSecurityViewLocation(leftAlign, animate);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 19a2d9e..61e2624 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -116,12 +116,8 @@
                 // If we're in one handed mode, the user can tap on the opposite side of the screen
                 // to move the bouncer across. In that case, inhibit the falsing (otherwise the taps
                 // to move the bouncer to each screen side can end up closing it instead).
-                if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) {
-                    boolean isLeftAligned = mView.isOneHandedModeLeftAligned();
-                    if ((isLeftAligned && ev.getX() > mView.getWidth() / 2f)
-                            || (!isLeftAligned && ev.getX() <= mView.getWidth() / 2f)) {
-                        mFalsingCollector.avoidGesture();
-                    }
+                if (mView.isTouchOnTheOtherSideOfSecurity(ev)) {
+                    mFalsingCollector.avoidGesture();
                 }
 
                 if (mTouchDown != null) {
@@ -169,8 +165,8 @@
 
         public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) {
             int bouncerSide = SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__DEFAULT;
-            if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) {
-                bouncerSide = mView.isOneHandedModeLeftAligned()
+            if (mView.isSidedSecurityMode()) {
+                bouncerSide = mView.isSecurityLeftAligned()
                         ? SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__LEFT
                         : SysUiStatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__SIDE__RIGHT;
             }
@@ -367,8 +363,8 @@
     public void onResume(int reason) {
         if (mCurrentSecurityMode != SecurityMode.None) {
             int state = SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN;
-            if (mView.getMode() == KeyguardSecurityContainer.MODE_ONE_HANDED) {
-                state = mView.isOneHandedModeLeftAligned()
+            if (mView.isSidedSecurityMode()) {
+                state = mView.isSecurityLeftAligned()
                         ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_LEFT
                         : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN_RIGHT;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
index 61b1b66..c595586 100644
--- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt
@@ -27,12 +27,15 @@
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.RectF
+import android.hardware.biometrics.BiometricSourceType
 import android.view.View
 import androidx.core.graphics.ColorUtils
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.settingslib.Utils
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import java.util.concurrent.Executor
 
 /**
  * When the face is enrolled, we use this view to show the face scanning animation and the camera
@@ -42,7 +45,8 @@
     context: Context,
     pos: Int,
     val statusBarStateController: StatusBarStateController,
-    val keyguardUpdateMonitor: KeyguardUpdateMonitor
+    val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    val mainExecutor: Executor
 ) : ScreenDecorations.DisplayCutoutView(context, pos) {
     private var showScanningAnim = false
     private val rimPaint = Paint()
@@ -54,11 +58,26 @@
             com.android.systemui.R.attr.wallpaperTextColorAccent)
     private var cameraProtectionAnimator: ValueAnimator? = null
     var hideOverlayRunnable: Runnable? = null
+    var faceAuthSucceeded = false
 
     init {
         visibility = View.INVISIBLE // only show this view when face scanning is happening
     }
 
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        mainExecutor.execute {
+            keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        mainExecutor.execute {
+            keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
+        }
+    }
+
     override fun setColor(color: Int) {
         cameraProtectionColor = color
         invalidate()
@@ -108,7 +127,6 @@
         if (showScanningAnimNow == showScanningAnim) {
             return
         }
-
         showScanningAnim = showScanningAnimNow
         updateProtectionBoundingPath()
         // Delay the relayout until the end of the animation when hiding,
@@ -120,13 +138,20 @@
 
         cameraProtectionAnimator?.cancel()
         cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress,
-                if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
-            startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION
-            duration = if (showScanningAnim) PULSE_APPEAR_DURATION else
-                CAMERA_PROTECTION_DISAPPEAR_DURATION
-            interpolator = if (showScanningAnim) Interpolators.STANDARD else
-                Interpolators.EMPHASIZED
-
+                if (showScanningAnimNow) SHOW_CAMERA_PROTECTION_SCALE
+                else HIDDEN_CAMERA_PROTECTION_SCALE).apply {
+            startDelay =
+                    if (showScanningAnim) 0
+                    else if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
+                    else PULSE_ERROR_DISAPPEAR_DURATION
+            duration =
+                    if (showScanningAnim) CAMERA_PROTECTION_APPEAR_DURATION
+                    else if (faceAuthSucceeded) CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION
+                    else CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION
+            interpolator =
+                    if (showScanningAnim) Interpolators.STANDARD_ACCELERATE
+                    else if (faceAuthSucceeded) Interpolators.STANDARD
+                    else Interpolators.STANDARD_DECELERATE
             addUpdateListener(ValueAnimator.AnimatorUpdateListener {
                 animation: ValueAnimator ->
                 cameraProtectionProgress = animation.animatedValue as Float
@@ -143,47 +168,73 @@
                     }
                 }
             })
-            start()
         }
 
         rimAnimator?.cancel()
         rimAnimator = AnimatorSet().apply {
-            val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress,
-                    if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply {
-                duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION
-                interpolator = Interpolators.STANDARD
-                addUpdateListener(ValueAnimator.AnimatorUpdateListener {
-                    animation: ValueAnimator ->
-                    rimProgress = animation.animatedValue as Float
-                    invalidate()
-                })
-            }
             if (showScanningAnim) {
-                // appear and then pulse in/out
-                playSequentially(rimAppearOrDisappearAnimator,
+                val rimAppearAnimator = ValueAnimator.ofFloat(SHOW_CAMERA_PROTECTION_SCALE,
+                        PULSE_RADIUS_OUT).apply {
+                    duration = PULSE_APPEAR_DURATION
+                    interpolator = Interpolators.STANDARD_DECELERATE
+                    addUpdateListener(ValueAnimator.AnimatorUpdateListener {
+                        animation: ValueAnimator ->
+                        rimProgress = animation.animatedValue as Float
+                        invalidate()
+                    })
+                }
+
+                // animate in camera protection, rim, and then pulse in/out
+                playSequentially(cameraProtectionAnimator, rimAppearAnimator,
                         createPulseAnimator(), createPulseAnimator(),
                         createPulseAnimator(), createPulseAnimator(),
                         createPulseAnimator(), createPulseAnimator())
             } else {
-                val opacityAnimator = ValueAnimator.ofInt(255, 0).apply {
-                    duration = PULSE_DISAPPEAR_DURATION
-                    interpolator = Interpolators.LINEAR
+                val rimDisappearAnimator = ValueAnimator.ofFloat(
+                        rimProgress,
+                        if (faceAuthSucceeded) PULSE_RADIUS_SUCCESS
+                        else SHOW_CAMERA_PROTECTION_SCALE
+                ).apply {
+                    duration =
+                            if (faceAuthSucceeded) PULSE_SUCCESS_DISAPPEAR_DURATION
+                            else PULSE_ERROR_DISAPPEAR_DURATION
+                    interpolator =
+                            if (faceAuthSucceeded) Interpolators.STANDARD_DECELERATE
+                            else Interpolators.STANDARD
                     addUpdateListener(ValueAnimator.AnimatorUpdateListener {
                         animation: ValueAnimator ->
-                        rimPaint.alpha = animation.animatedValue as Int
+                        rimProgress = animation.animatedValue as Float
                         invalidate()
                     })
+                    addListener(object : AnimatorListenerAdapter() {
+                        override fun onAnimationEnd(animation: Animator) {
+                            rimProgress = HIDDEN_RIM_SCALE
+                            invalidate()
+                        }
+                    })
                 }
-                addListener(object : AnimatorListenerAdapter() {
-                    override fun onAnimationEnd(animation: Animator) {
-                        rimProgress = HIDDEN_RIM_SCALE
-                        rimPaint.alpha = 255
-                        invalidate()
+                if (faceAuthSucceeded) {
+                    val successOpacityAnimator = ValueAnimator.ofInt(255, 0).apply {
+                        duration = PULSE_SUCCESS_DISAPPEAR_DURATION
+                        interpolator = Interpolators.LINEAR
+                        addUpdateListener(ValueAnimator.AnimatorUpdateListener {
+                            animation: ValueAnimator ->
+                            rimPaint.alpha = animation.animatedValue as Int
+                            invalidate()
+                        })
+                        addListener(object : AnimatorListenerAdapter() {
+                            override fun onAnimationEnd(animation: Animator) {
+                                rimPaint.alpha = 255
+                                invalidate()
+                            }
+                        })
                     }
-                })
-
-                // disappear
-                playTogether(rimAppearOrDisappearAnimator, opacityAnimator)
+                    val rimSuccessAnimator = AnimatorSet()
+                    rimSuccessAnimator.playTogether(rimDisappearAnimator, successOpacityAnimator)
+                    playTogether(rimSuccessAnimator, cameraProtectionAnimator)
+                } else {
+                    playTogether(rimDisappearAnimator, cameraProtectionAnimator)
+                }
             }
 
             addListener(object : AnimatorListenerAdapter() {
@@ -253,15 +304,72 @@
         }
     }
 
+    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
+        override fun onBiometricAuthenticated(
+            userId: Int,
+            biometricSourceType: BiometricSourceType?,
+            isStrongBiometric: Boolean
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = true
+                    enableShowProtection(true)
+                }
+            }
+        }
+
+        override fun onBiometricAcquired(
+            biometricSourceType: BiometricSourceType?,
+            acquireInfo: Int
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false // reset
+                }
+            }
+        }
+
+        override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false
+                    enableShowProtection(false)
+                }
+            }
+        }
+
+        override fun onBiometricError(
+            msgId: Int,
+            errString: String?,
+            biometricSourceType: BiometricSourceType?
+        ) {
+            if (biometricSourceType == BiometricSourceType.FACE) {
+                post {
+                    faceAuthSucceeded = false
+                    enableShowProtection(false)
+                }
+            }
+        }
+    }
+
     companion object {
         private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE
+        private const val SHOW_CAMERA_PROTECTION_SCALE = 1f
 
-        private const val PULSE_APPEAR_DURATION = 350L
+        private const val PULSE_RADIUS_IN = 1.1f
+        private const val PULSE_RADIUS_OUT = 1.125f
+        private const val PULSE_RADIUS_SUCCESS = 1.25f
+
+        private const val CAMERA_PROTECTION_APPEAR_DURATION = 250L
+        private const val PULSE_APPEAR_DURATION = 250L // without start delay
+
         private const val PULSE_DURATION_INWARDS = 500L
         private const val PULSE_DURATION_OUTWARDS = 500L
-        private const val PULSE_DISAPPEAR_DURATION = 850L
-        private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay
-        private const val PULSE_RADIUS_IN = 1.15f
-        private const val PULSE_RADIUS_OUT = 1.25f
+
+        private const val PULSE_SUCCESS_DISAPPEAR_DURATION = 400L
+        private const val CAMERA_PROTECTION_SUCCESS_DISAPPEAR_DURATION = 500L // without start delay
+
+        private const val PULSE_ERROR_DISAPPEAR_DURATION = 200L
+        private const val CAMERA_PROTECTION_ERROR_DISAPPEAR_DURATION = 300L // without start delay
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index ec4cf2f..24b8933 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -510,6 +510,7 @@
                     mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator
                             .aboutToShowBouncerProgress(fraction) : fraction;
             updateAlpha();
+            updatePauseAuth();
         }
     };
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index aeda20f..afc58ef 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -45,6 +45,7 @@
 import android.content.res.Resources;
 import android.hardware.SensorManager;
 import android.hardware.SensorPrivacyManager;
+import android.hardware.camera2.CameraManager;
 import android.hardware.devicestate.DeviceStateManager;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.display.ColorDisplayManager;
@@ -60,6 +61,7 @@
 import android.net.NetworkScoreManager;
 import android.net.wifi.WifiManager;
 import android.os.BatteryStats;
+import android.os.PowerExemptionManager;
 import android.os.PowerManager;
 import android.os.ServiceManager;
 import android.os.UserManager;
@@ -377,6 +379,13 @@
 
     /** */
     @Provides
+    @Singleton
+    static PowerExemptionManager providePowerExemptionManager(Context context) {
+        return context.getSystemService(PowerExemptionManager.class);
+    }
+
+    /** */
+    @Provides
     @Main
     public SharedPreferences provideSharePreferences(Context context) {
         return Prefs.get(context);
@@ -545,4 +554,10 @@
     static SafetyCenterManager provideSafetyCenterManager(Context context) {
         return context.getSystemService(SafetyCenterManager.class);
     }
+
+    @Provides
+    @Singleton
+    static CameraManager provideCameraManager(Context context) {
+        return context.getSystemService(CameraManager.class);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index adc0096..81d3d6c 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -32,10 +32,12 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.FaceScanningOverlay
 import com.android.systemui.biometrics.AuthController
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import java.util.concurrent.Executor
 import javax.inject.Inject
 
 @SysUISingleton
@@ -44,6 +46,7 @@
     private val context: Context,
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    @Main private val mainExecutor: Executor,
     private val featureFlags: FeatureFlags
 ) : DecorProviderFactory() {
     private val display = context.display
@@ -82,7 +85,9 @@
                                         bound.baseOnRotation0(displayInfo.rotation),
                                         authController,
                                         statusBarStateController,
-                                        keyguardUpdateMonitor)
+                                        keyguardUpdateMonitor,
+                                        mainExecutor
+                                )
                         )
                     }
                 }
@@ -102,7 +107,8 @@
     override val alignedBound: Int,
     private val authController: AuthController,
     private val statusBarStateController: StatusBarStateController,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val mainExecutor: Executor
 ) : BoundDecorProvider() {
     override val viewId: Int = com.android.systemui.R.id.face_scanning_anim
 
@@ -127,7 +133,9 @@
                 context,
                 alignedBound,
                 statusBarStateController,
-                keyguardUpdateMonitor)
+                keyguardUpdateMonitor,
+                mainExecutor
+        )
         view.id = viewId
         FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                 ViewGroup.LayoutParams.MATCH_PARENT).let {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index fc71e2f..69e41ba 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -101,6 +101,9 @@
     public void addComplication(Complication complication) {
         mExecutor.execute(() -> {
             if (mComplications.add(complication)) {
+                if (DEBUG) {
+                    Log.d(TAG, "addComplication: added " + complication);
+                }
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
@@ -112,6 +115,9 @@
     public void removeComplication(Complication complication) {
         mExecutor.execute(() -> {
             if (mComplications.remove(complication)) {
+                if (DEBUG) {
+                    Log.d(TAG, "removeComplication: removed " + complication);
+                }
                 mCallbacks.stream().forEach(callback -> callback.onComplicationsChanged());
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java
index 486fc89..be94e50 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java
@@ -82,6 +82,7 @@
                         mSmartSpaceController.addListener(mSmartspaceListener);
                     } else {
                         mSmartSpaceController.removeListener(mSmartspaceListener);
+                        mDreamOverlayStateController.removeComplication(mComplication);
                     }
                 }
             });
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
index c1173ae..fd6cfc0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -21,6 +21,7 @@
 
 import android.graphics.Rect;
 import android.graphics.Region;
+import android.os.Debug;
 import android.util.Log;
 import android.view.View;
 
@@ -44,7 +45,8 @@
  * a {@link ComplicationLayoutEngine}.
  */
 public class ComplicationHostViewController extends ViewController<ConstraintLayout> {
-    public static final String TAG = "ComplicationHostVwCtrl";
+    private static final String TAG = "ComplicationHostVwCtrl";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final ComplicationLayoutEngine mLayoutEngine;
     private final LifecycleOwner mLifecycleOwner;
@@ -90,6 +92,11 @@
     }
 
     private void updateComplications(Collection<ComplicationViewModel> complications) {
+        if (DEBUG) {
+            Log.d(TAG, "updateComplications called. Callers = " + Debug.getCallers(25));
+            Log.d(TAG, "    mComplications = " + mComplications.toString());
+            Log.d(TAG, "    complications = " + complications.toString());
+        }
         final Collection<ComplicationId> ids = complications.stream()
                 .map(complicationViewModel -> complicationViewModel.getId())
                 .collect(Collectors.toSet());
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
index ded61a8..9cd149b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -54,7 +54,7 @@
  */
 @DreamOverlayComponent.DreamOverlayScope
 public class ComplicationLayoutEngine implements Complication.VisibilityController {
-    public static final String TAG = "ComplicationLayoutEngine";
+    public static final String TAG = "ComplicationLayoutEng";
 
     /**
      * {@link ViewEntry} is an internal container, capturing information necessary for working with
@@ -529,7 +529,7 @@
      */
     public void addComplication(ComplicationId id, View view,
             ComplicationLayoutParams lp, @Complication.Category int category) {
-        Log.d(TAG, "engine: " + this + " addComplication");
+        Log.d(TAG, "@" + Integer.toHexString(this.hashCode()) + " addComplication: " + id);
 
         // If the complication is present, remove.
         if (mEntries.containsKey(id)) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java
index f023937..00cf58c 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationViewModel.java
@@ -64,4 +64,9 @@
     public void exitDream() {
         mHost.requestExitDream();
     }
+
+    @Override
+    public String toString() {
+        return mId + "=" + mComplication.toString();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
index 9789cef..63f63a5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamSmartspaceController.kt
@@ -145,9 +145,6 @@
             if (view !is View) {
                 return null
             }
-
-            view.setIsDreaming(true)
-
             return view
         } else {
             null
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 47c678b..47a68bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -60,6 +60,9 @@
     public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
             new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
 
+    public static final BooleanFlag REMOVE_UNRANKED_NOTIFICATIONS =
+            new BooleanFlag(109, false);
+
     /***************************************/
     // 200 - keyguard/lockscreen
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index b96eee7..95b3b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -55,6 +55,7 @@
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.Trace;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Slog;
 import android.view.IRemoteAnimationFinishedCallback;
@@ -157,7 +158,7 @@
             Rect localBounds = new Rect(change.getEndAbsBounds());
             localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
 
-            out.add(new RemoteAnimationTarget(
+            final RemoteAnimationTarget target = new RemoteAnimationTarget(
                     taskId,
                     newModeToLegacyMode(change.getMode()),
                     change.getLeash(),
@@ -168,7 +169,15 @@
                     info.getChanges().size() - i,
                     new Point(), localBounds, new Rect(change.getEndAbsBounds()),
                     windowConfiguration, isNotInRecents, null /* startLeash */,
-                    change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */));
+                    change.getStartAbsBounds(), taskInfo, false /* allowEnterPip */);
+            // Use hasAnimatingParent to mark the anything below root task
+            if (taskId != -1 && change.getParent() != null) {
+                final TransitionInfo.Change parentChange = info.getChange(change.getParent());
+                if (parentChange != null && parentChange.getTaskInfo() != null) {
+                    target.hasAnimatingParent = true;
+                }
+            }
+            out.add(target);
         }
         return out.toArray(new RemoteAnimationTarget[out.size()]);
     }
@@ -189,8 +198,12 @@
         }
     }
 
+    // Wrap Keyguard going away animation
     private static IRemoteTransition wrap(IRemoteAnimationRunner runner) {
         return new IRemoteTransition.Stub() {
+            final ArrayMap<IBinder, IRemoteTransitionFinishedCallback> mFinishCallbacks =
+                    new ArrayMap<>();
+
             @Override
             public void startAnimation(IBinder transition, TransitionInfo info,
                     SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback)
@@ -200,16 +213,37 @@
                 final RemoteAnimationTarget[] wallpapers = wrap(info, true /* wallpapers */);
                 final RemoteAnimationTarget[] nonApps = new RemoteAnimationTarget[0];
 
-                // TODO: Remove this, and update alpha value in the IAnimationRunner.
-                for (TransitionInfo.Change change : info.getChanges()) {
-                    t.setAlpha(change.getLeash(), 1.0f);
+                // Sets the alpha to 0 for the opening root task for fade in animation. And since
+                // the fade in animation can only apply on the first opening app, so set alpha to 1
+                // for anything else.
+                boolean foundOpening = false;
+                for (RemoteAnimationTarget target : apps) {
+                    if (target.taskId != -1
+                            && target.mode == RemoteAnimationTarget.MODE_OPENING
+                            && !target.hasAnimatingParent) {
+                        if (foundOpening) {
+                            Log.w(TAG, "More than one opening target");
+                            t.setAlpha(target.leash, 1.0f);
+                            continue;
+                        }
+                        t.setAlpha(target.leash, 0.0f);
+                        foundOpening = true;
+                    } else {
+                        t.setAlpha(target.leash, 1.0f);
+                    }
                 }
                 t.apply();
+                synchronized (mFinishCallbacks) {
+                    mFinishCallbacks.put(transition, finishCallback);
+                }
                 runner.onAnimationStart(getTransitionOldType(info.getType(), info.getFlags(), apps),
                         apps, wallpapers, nonApps,
                         new IRemoteAnimationFinishedCallback.Stub() {
                             @Override
                             public void onAnimationFinished() throws RemoteException {
+                                synchronized (mFinishCallbacks) {
+                                    if (mFinishCallbacks.remove(transition) == null) return;
+                                }
                                 Slog.d(TAG, "Finish IRemoteAnimationRunner.");
                                 finishCallback.onTransitionFinished(null /* wct */, null /* t */);
                             }
@@ -220,7 +254,20 @@
             public void mergeAnimation(IBinder transition, TransitionInfo info,
                     SurfaceControl.Transaction t, IBinder mergeTarget,
                     IRemoteTransitionFinishedCallback finishCallback) {
-
+                try {
+                    final IRemoteTransitionFinishedCallback origFinishCB;
+                    synchronized (mFinishCallbacks) {
+                        origFinishCB = mFinishCallbacks.remove(transition);
+                    }
+                    if (origFinishCB == null) {
+                        // already finished (or not started yet), so do nothing.
+                        return;
+                    }
+                    runner.onAnimationCancelled(false /* isKeyguardOccluded */);
+                    origFinishCB.onTransitionFinished(null /* wct */, null /* t */);
+                } catch (RemoteException e) {
+                    // nothing, we'll just let it finish on its own I guess.
+                }
             }
         };
     }
@@ -349,7 +396,7 @@
         }
 
         @Override // Binder interface
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
             mKeyguardViewMediator.cancelKeyguardExitAnimation();
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 99b5720..382323f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -26,6 +26,7 @@
 import android.os.RemoteException
 import android.util.Log
 import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
 import android.view.SyncRtSurfaceTransactionApplier
 import android.view.View
 import androidx.annotation.VisibleForTesting
@@ -293,6 +294,8 @@
 
     private val handler = Handler()
 
+    private val tmpFloat = FloatArray(9)
+
     init {
         with(surfaceBehindAlphaAnimator) {
             duration = SURFACE_BEHIND_SWIPE_FADE_DURATION_MS
@@ -723,13 +726,27 @@
             if (keyguardStateController.isSnappingKeyguardBackAfterSwipe) amount
             else surfaceBehindAlpha
 
-        applyParamsToSurface(
-            SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
-                surfaceBehindRemoteAnimationTarget!!.leash)
-                .withMatrix(surfaceBehindMatrix)
-                .withCornerRadius(roundedCornerRadius)
-                .withAlpha(animationAlpha)
-                .build())
+        // SyncRtSurfaceTransactionApplier cannot apply transaction when the target view is unable
+        // to draw
+        val sc: SurfaceControl? = surfaceBehindRemoteAnimationTarget?.leash
+        if (keyguardViewController.viewRootImpl.view?.visibility != View.VISIBLE &&
+            sc?.isValid == true) {
+            with(SurfaceControl.Transaction()) {
+                setMatrix(sc, surfaceBehindMatrix, tmpFloat)
+                setCornerRadius(sc, roundedCornerRadius)
+                setAlpha(sc, animationAlpha)
+                apply()
+            }
+        } else {
+            applyParamsToSurface(
+                SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
+                    surfaceBehindRemoteAnimationTarget!!.leash)
+                    .withMatrix(surfaceBehindMatrix)
+                    .withCornerRadius(roundedCornerRadius)
+                    .withAlpha(animationAlpha)
+                    .build()
+            )
+        }
     }
 
     /**
@@ -744,8 +761,11 @@
         handler.removeCallbacksAndMessages(null)
 
         // Make sure we made the surface behind fully visible, just in case. It should already be
-        // fully visible. If the launcher is doing its own animation, let it continue without
-        // forcing it to 1f.
+        // fully visible. The exit animation is finished, and we should not hold the leash anymore,
+        // so forcing it to 1f.
+        surfaceBehindAlphaAnimator.cancel()
+        surfaceBehindEntryAnimator.cancel()
+        surfaceBehindAlpha = 1f
         setSurfaceBehindAppearAmount(1f)
         launcherUnlockController?.setUnlockAmount(1f, false /* forceIfAnimating */)
 
@@ -910,4 +930,4 @@
             return context.resources.getIntArray(R.array.config_foldedDeviceStates).isNotEmpty()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index f9a1c66..340cde1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -843,7 +843,6 @@
 
                 @Override
                 public void onLaunchAnimationCancelled() {
-                    setOccluded(true /* occluded */, false /* animate */);
                     Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
                             + mOccluded);
                 }
@@ -911,12 +910,12 @@
                 private final Matrix mUnoccludeMatrix = new Matrix();
 
                 @Override
-                public void onAnimationCancelled() {
+                public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     if (mUnoccludeAnimator != null) {
                         mUnoccludeAnimator.cancel();
                     }
 
-                    setOccluded(false /* isOccluded */, false /* animate */);
+                    setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */);
                     Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: "
                             + mOccluded);
                 }
@@ -2503,10 +2502,18 @@
                 mInteractionJankMonitor.begin(
                         createInteractionJankMonitorConf("DismissPanel"));
 
+                // Apply the opening animation on root task if exists
+                RemoteAnimationTarget aniTarget = apps[0];
+                for (RemoteAnimationTarget tmpTarget : apps) {
+                    if (tmpTarget.taskId != -1 && !tmpTarget.hasAnimatingParent) {
+                        aniTarget = tmpTarget;
+                        break;
+                    }
+                }
                 // Pass the surface and metadata to the unlock animation controller.
                 mKeyguardUnlockAnimationControllerLazy.get()
                         .notifyStartSurfaceBehindRemoteAnimation(
-                                apps[0], startTime, mSurfaceBehindRemoteAnimationRequested);
+                                aniTarget, startTime, mSurfaceBehindRemoteAnimationRequested);
             } else {
                 mInteractionJankMonitor.begin(
                         createInteractionJankMonitorConf("RemoteAnimationDisabled"));
@@ -3167,9 +3174,9 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
             if (mRunner != null) {
-                mRunner.onAnimationCancelled();
+                mRunner.onAnimationCancelled(isKeyguardOccluded);
             }
         }
 
@@ -3210,9 +3217,12 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
-            super.onAnimationCancelled();
-            Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: " + mOccluded);
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
+            super.onAnimationCancelled(isKeyguardOccluded);
+            setOccluded(isKeyguardOccluded /* occluded */, false /* animate */);
+
+            Log.d(TAG, "Occlude animation cancelled by WM. "
+                    + "Setting occluded state to: " + mOccluded);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index ed5c193..2f732de 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -23,6 +23,7 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.util.Log
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
@@ -48,6 +49,8 @@
 import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
+private val TAG: String = MediaHierarchyManager::class.java.simpleName
+
 /**
  * Similarly to isShown but also excludes views that have 0 alpha
  */
@@ -964,6 +967,14 @@
                         top,
                         left + currentBounds.width(),
                         top + currentBounds.height())
+
+                if (mediaFrame.childCount > 0) {
+                    val child = mediaFrame.getChildAt(0)
+                    if (mediaFrame.height < child.height) {
+                        Log.wtf(TAG, "mediaFrame height is too small for child: " +
+                            "${mediaFrame.height} vs ${child.height}")
+                    }
+                }
             }
             if (isCrossFadeAnimatorRunning) {
                 // When cross-fading with an animation, we only notify the media carousel of the
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index f0ce30d..f2f2753 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -285,6 +285,7 @@
                 Log.d(TAG, "This device is already connected! : " + device.getName());
                 return;
             }
+            mController.setTemporaryAllowListExceptionIfNeeded(device);
             mCurrentActivePosition = -1;
             mController.connectDevice(device);
             device.setState(MediaDeviceState.STATE_CONNECTING);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
index ccc0a3d..bec6739 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -69,11 +69,13 @@
     View mHolderView;
     boolean mIsDragging;
     int mCurrentActivePosition;
+    private boolean mIsInitVolumeFirstTime;
 
     public MediaOutputBaseAdapter(MediaOutputController controller) {
         mController = controller;
         mIsDragging = false;
         mCurrentActivePosition = -1;
+        mIsInitVolumeFirstTime = true;
     }
 
     @Override
@@ -275,7 +277,7 @@
             mSeekBar.setMaxVolume(device.getMaxVolume());
             final int currentVolume = device.getCurrentVolume();
             if (mSeekBar.getVolume() != currentVolume) {
-                if (isCurrentSeekbarInvisible) {
+                if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
                     animateCornerAndVolume(mSeekBar.getProgress(),
                             MediaOutputSeekbar.scaleVolumeToProgress(currentVolume));
                 } else {
@@ -284,6 +286,9 @@
                     }
                 }
             }
+            if (mIsInitVolumeFirstTime) {
+                mIsInitVolumeFirstTime = false;
+            }
             mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                 @Override
                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
index 38005db..0fa3265 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogFactory.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
+import android.os.PowerExemptionManager
 import android.view.View
 import com.android.internal.logging.UiEventLogger
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -43,7 +44,8 @@
     private val uiEventLogger: UiEventLogger,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
-    private val audioManager: AudioManager
+    private val audioManager: AudioManager,
+    private val powerExemptionManager: PowerExemptionManager
 ) {
     var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
 
@@ -54,7 +56,8 @@
 
         val controller = MediaOutputController(context, packageName,
                 mediaSessionManager, lbm, starter, notifCollection,
-                dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager)
+                dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
+                powerExemptionManager)
         val dialog =
                 MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
         mediaOutputBroadcastDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 0b4b036..247ffa7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -44,6 +44,7 @@
 import android.media.session.MediaSessionManager;
 import android.media.session.PlaybackState;
 import android.os.IBinder;
+import android.os.PowerExemptionManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -101,6 +102,9 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final String PAGE_CONNECTED_DEVICES_KEY =
             "top_level_connected_devices";
+    private static final long ALLOWLIST_DURATION_MS = 20000;
+    private static final String ALLOWLIST_REASON = "mediaoutput:remote_transfer";
+
     private final String mPackageName;
     private final Context mContext;
     private final MediaSessionManager mMediaSessionManager;
@@ -114,6 +118,7 @@
     final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
     final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
     private final AudioManager mAudioManager;
+    private final PowerExemptionManager mPowerExemptionManager;
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
@@ -147,7 +152,8 @@
             CommonNotifCollection notifCollection,
             DialogLaunchAnimator dialogLaunchAnimator,
             Optional<NearbyMediaDevicesManager> nearbyMediaDevicesManagerOptional,
-            AudioManager audioManager) {
+            AudioManager audioManager,
+            PowerExemptionManager powerExemptionManager) {
         mContext = context;
         mPackageName = packageName;
         mMediaSessionManager = mediaSessionManager;
@@ -155,6 +161,7 @@
         mActivityStarter = starter;
         mNotifCollection = notifCollection;
         mAudioManager = audioManager;
+        mPowerExemptionManager = powerExemptionManager;
         InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
         mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
         mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
@@ -776,7 +783,7 @@
         MediaOutputController controller = new MediaOutputController(mContext, mPackageName,
                 mMediaSessionManager, mLocalBluetoothManager, mActivityStarter,
                 mNotifCollection, mDialogLaunchAnimator, Optional.of(mNearbyMediaDevicesManager),
-                mAudioManager);
+                mAudioManager, mPowerExemptionManager);
         MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
                 broadcastSender, controller);
         mDialogLaunchAnimator.showFromView(dialog, mediaOutputDialog);
@@ -822,6 +829,17 @@
         broadcast.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8));
     }
 
+    void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) {
+        if (mPowerExemptionManager == null || mPackageName == null) {
+            Log.w(TAG, "powerExemptionManager or package name is null");
+            return;
+        }
+        mPowerExemptionManager.addToTemporaryAllowList(mPackageName,
+                PowerExemptionManager.REASON_MEDIA_NOTIFICATION_TRANSFER,
+                ALLOWLIST_REASON,
+                ALLOWLIST_DURATION_MS);
+    }
+
     String getBroadcastMetadata() {
         LocalBluetoothLeBroadcast broadcast =
                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 8701d4a..8249a7c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.media.AudioManager
 import android.media.session.MediaSessionManager
+import android.os.PowerExemptionManager
 import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.UiEventLogger
@@ -45,7 +46,8 @@
     private val uiEventLogger: UiEventLogger,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
     private val nearbyMediaDevicesManagerOptional: Optional<NearbyMediaDevicesManager>,
-    private val audioManager: AudioManager
+    private val audioManager: AudioManager,
+    private val powerExemptionManager: PowerExemptionManager
 ) {
     companion object {
         private const val INTERACTION_JANK_TAG = "media_output"
@@ -60,8 +62,8 @@
         val controller = MediaOutputController(
             context, packageName,
             mediaSessionManager, lbm, starter, notifCollection,
-            dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager
-        )
+            dialogLaunchAnimator, nearbyMediaDevicesManagerOptional, audioManager,
+            powerExemptionManager)
         val dialog =
             MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller, uiEventLogger)
         mediaOutputDialog = dialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java
index 7c04810..2c35db3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamComplication.java
@@ -37,11 +37,6 @@
     }
 
     @Override
-    public int getRequiredTypeAvailability() {
-        return COMPLICATION_TYPE_CAST_INFO;
-    }
-
-    @Override
     public ViewHolder createView(ComplicationViewModel model) {
         return mComponentFactory.create().getViewHolder();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
index 3cc99a8..e95976f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
@@ -27,4 +27,4 @@
     fun getTimeoutMs(): Long
 }
 
-const val DEFAULT_TIMEOUT_MILLIS = 3000L
+const val DEFAULT_TIMEOUT_MILLIS = 4000L
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index 7cc52e4..0f1cdcc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -25,13 +25,14 @@
 import android.os.PowerManager
 import android.os.SystemClock
 import android.util.Log
-import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.MotionEvent
-import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
-import android.widget.LinearLayout
+import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
+import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
+import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
 import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
@@ -56,16 +57,20 @@
     private val windowManager: WindowManager,
     private val viewUtil: ViewUtil,
     @Main private val mainExecutor: DelayableExecutor,
+    private val accessibilityManager: AccessibilityManager,
     private val tapGestureDetector: TapGestureDetector,
     private val powerManager: PowerManager,
     @LayoutRes private val chipLayoutRes: Int
 ) {
-    /** The window layout parameters we'll use when attaching the view to a window. */
+
+    /**
+     * Window layout params that will be used as a starting point for the [windowLayoutParams] of
+     * all subclasses.
+     */
     @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
-    private val windowLayoutParams = WindowManager.LayoutParams().apply {
+    internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
         width = WindowManager.LayoutParams.WRAP_CONTENT
         height = WindowManager.LayoutParams.WRAP_CONTENT
-        gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
         type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
         flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
         title = WINDOW_TITLE
@@ -73,6 +78,14 @@
         setTrustedOverlay()
     }
 
+    /**
+     * The window layout parameters we'll use when attaching the view to a window.
+     *
+     * Subclasses must override this to provide their specific layout params, and they should use
+     * [commonWindowLayoutParams] as part of their layout params.
+     */
+    internal abstract val windowLayoutParams: WindowManager.LayoutParams
+
     /** The chip view currently being displayed. Null if the chip is not being displayed. */
     private var chipView: ViewGroup? = null
 
@@ -110,10 +123,16 @@
         }
 
         // Cancel and re-set the chip timeout each time we get a new state.
+        val timeout = accessibilityManager.getRecommendedTimeoutMillis(
+            chipInfo.getTimeoutMs().toInt(),
+            // Not all chips have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
+            // include it just to be safe.
+            FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
+       )
         cancelChipViewTimeout?.run()
         cancelChipViewTimeout = mainExecutor.executeDelayed(
             { removeChip(MediaTttRemovalReason.REASON_TIMEOUT) },
-            chipInfo.getTimeoutMs()
+            timeout.toLong()
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 072263f..f9818f0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -24,8 +24,11 @@
 import android.os.Handler
 import android.os.PowerManager
 import android.util.Log
+import android.view.Gravity
+import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
@@ -35,6 +38,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.util.animation.AnimationUtil.Companion.frames
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 import javax.inject.Inject
@@ -52,6 +56,7 @@
     windowManager: WindowManager,
     viewUtil: ViewUtil,
     mainExecutor: DelayableExecutor,
+    accessibilityManager: AccessibilityManager,
     tapGestureDetector: TapGestureDetector,
     powerManager: PowerManager,
     @Main private val mainHandler: Handler,
@@ -62,10 +67,16 @@
     windowManager,
     viewUtil,
     mainExecutor,
+    accessibilityManager,
     tapGestureDetector,
     powerManager,
     R.layout.media_ttt_chip_receiver
 ) {
+    override val windowLayoutParams = commonWindowLayoutParams.apply {
+        height = getWindowHeight()
+        gravity = Gravity.BOTTOM.or(Gravity.CENTER_HORIZONTAL)
+    }
+
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
         override fun updateMediaTapToTransferReceiverDisplay(
             @StatusBarManager.MediaTransferReceiverState displayState: Int,
@@ -128,6 +139,19 @@
         )
     }
 
+    override fun animateChipIn(chipView: ViewGroup) {
+        val appIconView = chipView.requireViewById<View>(R.id.app_icon)
+        appIconView.animate()
+                .translationYBy(-1 * getTranslationAmount().toFloat())
+                .setDuration(30.frames)
+                .start()
+        appIconView.animate()
+                .alpha(1f)
+                .setDuration(5.frames)
+                .start()
+
+    }
+
     override fun getIconSize(isAppIcon: Boolean): Int? =
         context.resources.getDimensionPixelSize(
             if (isAppIcon) {
@@ -136,6 +160,17 @@
                 R.dimen.media_ttt_generic_icon_size_receiver
             }
         )
+
+    private fun getWindowHeight(): Int {
+        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) +
+                // Make the window large enough to accommodate the animation amount
+                getTranslationAmount()
+    }
+
+    /** Returns the amount that the chip will be translated by in its intro animation. */
+    private fun getTranslationAmount(): Int {
+        return context.resources.getDimensionPixelSize(R.dimen.media_ttt_receiver_vert_translation)
+    }
 }
 
 data class ChipReceiverInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 54b4380..797a770 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -21,9 +21,11 @@
 import android.media.MediaRoute2Info
 import android.os.PowerManager
 import android.util.Log
+import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
 import android.widget.TextView
 import com.android.internal.statusbar.IUndoMediaTransferCallback
 import com.android.systemui.R
@@ -53,6 +55,7 @@
     windowManager: WindowManager,
     viewUtil: ViewUtil,
     @Main mainExecutor: DelayableExecutor,
+    accessibilityManager: AccessibilityManager,
     tapGestureDetector: TapGestureDetector,
     powerManager: PowerManager,
     private val uiEventLogger: MediaTttSenderUiEventLogger
@@ -62,10 +65,15 @@
     windowManager,
     viewUtil,
     mainExecutor,
+    accessibilityManager,
     tapGestureDetector,
     powerManager,
     R.layout.media_ttt_chip
 ) {
+    override val windowLayoutParams = commonWindowLayoutParams.apply {
+        gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
+    }
+
     private var currentlyDisplayedChipState: ChipStateSender? = null
 
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
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 f1dd5ff..8179d17 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -291,7 +291,8 @@
 
     override fun onMotionEvent(event: MotionEvent) {
         backAnimation?.onBackMotion(
-            event,
+            event.x,
+            event.y,
             event.actionMasked,
             if (mView.isLeftPanel) BackEvent.EDGE_LEFT else BackEvent.EDGE_RIGHT
         )
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 e210d68..3039d9d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -84,13 +84,16 @@
 import com.android.systemui.tracing.nano.EdgeBackGestureHandlerProto;
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
 import com.android.wm.shell.back.BackAnimation;
+import com.android.wm.shell.pip.Pip;
 
 import java.io.PrintWriter;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -147,16 +150,6 @@
                 mPackageName = "_UNKNOWN";
             }
         }
-
-        @Override
-        public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
-            mIsInPipMode = true;
-        }
-
-        @Override
-        public void onActivityUnpinned() {
-            mIsInPipMode = false;
-        }
     };
 
     private DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener =
@@ -188,6 +181,7 @@
     private final ViewConfiguration mViewConfiguration;
     private final WindowManager mWindowManager;
     private final IWindowManager mWindowManagerService;
+    private final Optional<Pip> mPipOptional;
     private final FalsingManager mFalsingManager;
     // Activities which should not trigger Back gesture.
     private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>();
@@ -218,6 +212,8 @@
     // We temporarily disable back gesture when user is quickswitching
     // between apps of different orientations
     private boolean mDisabledForQuickstep;
+    // This gets updated when the value of PipTransitionState#isInPip changes.
+    private boolean mIsInPip;
 
     private final PointF mDownPoint = new PointF();
     private final PointF mEndPoint = new PointF();
@@ -233,7 +229,6 @@
     private boolean mIsNavBarShownTransiently;
     private boolean mIsBackGestureAllowed;
     private boolean mGestureBlockingActivityRunning;
-    private boolean mIsInPipMode;
     private boolean mIsNewBackAffordanceEnabled;
 
     private InputMonitor mInputMonitor;
@@ -302,6 +297,8 @@
         }
     };
 
+    private final Consumer<Boolean> mOnIsInPipStateChangedListener =
+            (isInPip) -> mIsInPip = isInPip;
 
     EdgeBackGestureHandler(
             Context context,
@@ -316,6 +313,7 @@
             ViewConfiguration viewConfiguration,
             WindowManager windowManager,
             IWindowManager windowManagerService,
+            Optional<Pip> pipOptional,
             FalsingManager falsingManager,
             LatencyTracker latencyTracker,
             FeatureFlags featureFlags) {
@@ -332,6 +330,7 @@
         mViewConfiguration = viewConfiguration;
         mWindowManager = windowManager;
         mWindowManagerService = windowManagerService;
+        mPipOptional = pipOptional;
         mFalsingManager = falsingManager;
         mLatencyTracker = latencyTracker;
         mFeatureFlags = featureFlags;
@@ -491,6 +490,7 @@
             mPluginManager.removePluginListener(this);
             TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
             DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener);
+            mPipOptional.ifPresent(pip -> pip.setOnIsInPipStateChangedListener(null));
 
             try {
                 mWindowManagerService.unregisterSystemGestureExclusionListener(
@@ -508,6 +508,8 @@
             TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
             DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
                     mMainExecutor::execute, mOnPropertiesChangedListener);
+            mPipOptional.ifPresent(
+                    pip -> pip.setOnIsInPipStateChangedListener(mOnIsInPipStateChangedListener));
 
             try {
                 mWindowManagerService.registerSystemGestureExclusionListener(
@@ -680,7 +682,7 @@
     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 = mIsInPipMode && mPipExcludedBounds.contains(x, y);
+        final boolean isInsidePip = mIsInPip && mPipExcludedBounds.contains(x, y);
         if (isInsidePip || mNavBarOverlayExcludedBounds.contains(x, y)) {
             return false;
         }
@@ -933,7 +935,7 @@
         pw.println("  mInRejectedExclusion=" + mInRejectedExclusion);
         pw.println("  mExcludeRegion=" + mExcludeRegion);
         pw.println("  mUnrestrictedExcludeRegion=" + mUnrestrictedExcludeRegion);
-        pw.println("  mIsInPipMode=" + mIsInPipMode);
+        pw.println("  mIsInPip=" + mIsInPip);
         pw.println("  mPipExcludedBounds=" + mPipExcludedBounds);
         pw.println("  mNavBarOverlayExcludedBounds=" + mNavBarOverlayExcludedBounds);
         pw.println("  mEdgeWidthLeft=" + mEdgeWidthLeft);
@@ -1002,6 +1004,7 @@
         private final ViewConfiguration mViewConfiguration;
         private final WindowManager mWindowManager;
         private final IWindowManager mWindowManagerService;
+        private final Optional<Pip> mPipOptional;
         private final FalsingManager mFalsingManager;
         private final LatencyTracker mLatencyTracker;
         private final FeatureFlags mFeatureFlags;
@@ -1018,6 +1021,7 @@
                        ViewConfiguration viewConfiguration,
                        WindowManager windowManager,
                        IWindowManager windowManagerService,
+                       Optional<Pip> pipOptional,
                        FalsingManager falsingManager,
                        LatencyTracker latencyTracker,
                        FeatureFlags featureFlags) {
@@ -1032,6 +1036,7 @@
             mViewConfiguration = viewConfiguration;
             mWindowManager = windowManager;
             mWindowManagerService = windowManagerService;
+            mPipOptional = pipOptional;
             mFalsingManager = falsingManager;
             mLatencyTracker = latencyTracker;
             mFeatureFlags = featureFlags;
@@ -1052,6 +1057,7 @@
                     mViewConfiguration,
                     mWindowManager,
                     mWindowManagerService,
+                    mPipOptional,
                     mFalsingManager,
                     mLatencyTracker,
                     mFeatureFlags);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index a74c596..eba9d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -486,7 +486,7 @@
     public void onMotionEvent(MotionEvent event) {
         if (mBackAnimation != null) {
             mBackAnimation.onBackMotion(
-                    event,
+                    event.getX(), event.getY(),
                     event.getActionMasked(),
                     mIsLeftPanel ? BackEvent.EDGE_LEFT : BackEvent.EDGE_RIGHT);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 892c283..0288c9f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -342,7 +342,8 @@
         }
 
         val addedPackages = runningServiceTokens.keys.filter {
-            it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true
+            currentProfileIds.contains(it.userId) &&
+                    it.uiControl != UIControl.HIDE_ENTRY && runningApps[it]?.stopped != true
         }
         val removedPackages = runningApps.keys.filter { !runningServiceTokens.containsKey(it) }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 5d2060d..7b1ddd6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -139,12 +139,11 @@
 
     void updateResources(QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
-        int bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
                 QSUtils.getQsHeaderSystemIconsAreaHeight(mContext),
                 mQSPanelContainer.getPaddingEnd(),
-                bottomPadding);
+                mQSPanelContainer.getPaddingBottom());
 
         int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
         int horizontalPadding = getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 41724ef..324c019 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -362,11 +362,11 @@
     protected void updatePadding() {
         final Resources res = mContext.getResources();
         int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
-        // Bottom padding only when there's a new footer with its height.
+        int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         setPaddingRelative(getPaddingStart(),
                 paddingTop,
                 getPaddingEnd(),
-                getPaddingBottom());
+                paddingBottom);
     }
 
     void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index fbdabc7..a1c66b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -396,7 +396,6 @@
     }
 
     void saveTilesToSettings(List<String> tileSpecs) {
-        if (tileSpecs.contains("work")) Log.wtfStack(TAG, "Saving work tile");
         mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
                 null /* tag */, false /* default */, mCurrentUser,
                 true /* overrideable by restore */);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 5b6e5ce..c213f19 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -142,7 +142,7 @@
                 }
 
                 @Override
-                public void onAnimationCancelled() {
+                public void onAnimationCancelled(boolean isKeyguardOccluded) {
                 }
             };
 
diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
index d3ae198..236ba1f 100644
--- a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt
@@ -56,6 +56,8 @@
         ):
                 BcSmartspaceDataPlugin.SmartspaceView {
             val ssView = plugin.getView(parent)
+            // Currently, this is only used to provide SmartspaceView on Dream surface.
+            ssView.setIsDreaming(true)
             ssView.registerDataProvider(plugin)
 
             ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter {
@@ -81,4 +83,4 @@
             return ssView
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
index 6cfbb43..07455a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
@@ -66,7 +66,7 @@
      * @param entry entry to show
      */
     public void showNotification(@NonNull NotificationEntry entry) {
-        mLogger.logShowNotification(entry.getKey());
+        mLogger.logShowNotification(entry);
         addAlertEntry(entry);
         updateNotification(entry.getKey(), true /* alert */);
         entry.setInterruption();
@@ -320,7 +320,7 @@
          * @param updatePostTime whether or not to refresh the post time
          */
         public void updateEntry(boolean updatePostTime) {
-            mLogger.logUpdateEntry(mEntry.getKey(), updatePostTime);
+            mLogger.logUpdateEntry(mEntry, updatePostTime);
 
             long currentTime = mClock.currentTimeMillis();
             mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 86f9fa1..9e02909 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -18,6 +18,7 @@
 
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_MANAGEMENT_DISCLOSURE;
+import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
@@ -407,7 +408,7 @@
                     organizationName);
         } else {
             return mDevicePolicyManager.getResources().getString(
-                    KEYGUARD_MANAGEMENT_DISCLOSURE,
+                    KEYGUARD_NAMED_MANAGEMENT_DISCLOSURE,
                     () -> packageResources.getString(
                             R.string.do_disclosure_with_name, organizationName),
                     organizationName);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
index 5df593b..558bcac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt
@@ -61,7 +61,7 @@
     private val systemClock: SystemClock,
     private val uiEventLogger: UiEventLogger
 ) {
-    private var pluggedIn: Boolean? = null
+    private var pluggedIn: Boolean = false
     private val rippleEnabled: Boolean = featureFlags.isEnabled(Flags.CHARGING_RIPPLE) &&
             !SystemProperties.getBoolean("persist.debug.suppress-charging-ripple", false)
     private var normalizedPortPosX: Float = context.resources.getFloat(
@@ -99,15 +99,17 @@
                 nowPluggedIn: Boolean,
                 charging: Boolean
             ) {
-                // Suppresses the ripple when the state change comes from wireless charging.
-                if (batteryController.isPluggedInWireless) {
+                // Suppresses the ripple when the state change comes from wireless charging or
+                // its dock.
+                if (batteryController.isPluggedInWireless ||
+                        batteryController.isChargingSourceDock) {
                     return
                 }
-                val wasPluggedIn = pluggedIn
-                pluggedIn = nowPluggedIn
-                if ((wasPluggedIn == null || !wasPluggedIn) && nowPluggedIn) {
+
+                if (!pluggedIn && nowPluggedIn) {
                     startRippleWithDebounce()
                 }
+                pluggedIn = nowPluggedIn
             }
         }
         batteryController.addCallback(batteryStateChangeCallback)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
index a0ccd57..1be4c04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/DynamicPrivacyController.java
@@ -80,7 +80,7 @@
 
     @VisibleForTesting
     boolean isDynamicPrivacyEnabled() {
-        return !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
+        return mLockscreenUserManager.userAllowsNotificationsInPublic(
                 mLockscreenUserManager.getCurrentUserId());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 478f7aa..c4947ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -56,4 +56,7 @@
     fun isSmartspaceDedupingEnabled(): Boolean =
             featureFlags.isEnabled(Flags.SMARTSPACE) &&
                     featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-}
\ No newline at end of file
+
+    fun removeUnrankedNotifs(): Boolean =
+        featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
index 2397005..52dcf02 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManagerLogger.kt
@@ -17,18 +17,32 @@
 package com.android.systemui.statusbar.notification
 
 import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
 import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.LogMessage
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 /** Logger for [NotificationEntryManager]. */
 class NotificationEntryManagerLogger @Inject constructor(
+    notifPipelineFlags: NotifPipelineFlags,
     @NotificationLog private val buffer: LogBuffer
 ) {
+    private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() }
+
+    private inline fun devLog(
+        level: LogLevel,
+        initializer: LogMessage.() -> Unit,
+        noinline printer: LogMessage.() -> String
+    ) {
+        if (Compile.IS_DEBUG && devLoggingEnabled) buffer.log(TAG, level, initializer, printer)
+    }
+
     fun logNotifAdded(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF ADDED $str1"
@@ -36,7 +50,7 @@
     }
 
     fun logNotifUpdated(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF UPDATED $str1"
@@ -44,7 +58,7 @@
     }
 
     fun logInflationAborted(key: String, status: String, reason: String) {
-        buffer.log(TAG, DEBUG, {
+        devLog(DEBUG, {
             str1 = key
             str2 = status
             str3 = reason
@@ -54,7 +68,7 @@
     }
 
     fun logNotifInflated(key: String, isNew: Boolean) {
-        buffer.log(TAG, DEBUG, {
+        devLog(DEBUG, {
             str1 = key
             bool1 = isNew
         }, {
@@ -63,7 +77,7 @@
     }
 
     fun logRemovalIntercepted(key: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
         }, {
             "NOTIF REMOVE INTERCEPTED for $str1"
@@ -71,7 +85,7 @@
     }
 
     fun logLifetimeExtended(key: String, extenderName: String, status: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
             str2 = extenderName
             str3 = status
@@ -81,7 +95,7 @@
     }
 
     fun logNotifRemoved(key: String, removedByUser: Boolean) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = key
             bool1 = removedByUser
         }, {
@@ -90,7 +104,7 @@
     }
 
     fun logFilterAndSort(reason: String) {
-        buffer.log(TAG, INFO, {
+        devLog(INFO, {
             str1 = reason
         }, {
             "FILTER AND SORT reason=$str1"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
index 63c37e9..ed80f33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
@@ -9,4 +9,6 @@
 juliatuttle@google.com
 lynhan@google.com
 steell@google.com
-yurilin@google.com
\ No newline at end of file
+yurilin@google.com
+
+per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
index 792ff8d..f6a572e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListDumper.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.collection;
 
+import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.NOT_DISMISSED;
 
@@ -52,7 +53,7 @@
                     sb,
                     true,
                     includeRecordKeeping,
-                    interactionTracker.hasUserInteractedWith(entry.getKey()));
+                    interactionTracker.hasUserInteractedWith(logKey(entry)));
             if (entry instanceof GroupEntry) {
                 GroupEntry ge = (GroupEntry) entry;
                 NotificationEntry summary = ge.getSummary();
@@ -63,7 +64,7 @@
                             sb,
                             true,
                             includeRecordKeeping,
-                            interactionTracker.hasUserInteractedWith(summary.getKey()));
+                            interactionTracker.hasUserInteractedWith(logKey(summary)));
                 }
                 List<NotificationEntry> children = ge.getChildren();
                 for (int childIndex = 0;  childIndex < children.size(); childIndex++) {
@@ -74,7 +75,7 @@
                             sb,
                             true,
                             includeRecordKeeping,
-                            interactionTracker.hasUserInteractedWith(child.getKey()));
+                            interactionTracker.hasUserInteractedWith(logKey(child)));
                 }
             }
         }
@@ -116,11 +117,11 @@
         sb.append(indent)
                 .append("[").append(index).append("] ")
                 .append(index.length() == 1 ? " " : "")
-                .append(entry.getKey());
+                .append(logKey(entry));
 
         if (includeParent) {
             sb.append(" (parent=")
-                    .append(entry.getParent() != null ? entry.getParent().getKey() : null)
+                    .append(logKey(entry.getParent()))
                     .append(")");
 
             NotificationEntry notifEntry = entry.getRepresentativeEntry();
@@ -185,8 +186,8 @@
 
             if (notifEntry.getAttachState().getSuppressedChanges().getParent() != null) {
                 rksb.append("suppressedParent=")
-                        .append(notifEntry.getAttachState().getSuppressedChanges()
-                                .getParent().getKey())
+                        .append(logKey(notifEntry.getAttachState().getSuppressedChanges()
+                                .getParent()))
                         .append(" ");
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index ecce1ba..e345aab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -89,6 +89,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -110,6 +111,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
@@ -157,6 +159,8 @@
     private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
     private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
 
+    private Set<String> mNotificationsWithoutRankings = Collections.emptySet();
+
     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
 
     private boolean mAttached = false;
@@ -267,13 +271,14 @@
             requireNonNull(stats);
             NotificationEntry storedEntry = mNotificationSet.get(entry.getKey());
             if (storedEntry == null) {
-                mLogger.logNonExistentNotifDismissed(entry.getKey());
+                mLogger.logNonExistentNotifDismissed(entry);
                 continue;
             }
             if (entry != storedEntry) {
                 throw mEulogizer.record(
                         new IllegalStateException("Invalid entry: "
-                                + "different stored and dismissed entries for " + entry.getKey()));
+                                + "different stored and dismissed entries for " + logKey(entry)
+                                + " stored=@" + Integer.toHexString(storedEntry.hashCode())));
             }
 
             if (entry.getDismissState() == DISMISSED) {
@@ -282,7 +287,7 @@
 
             updateDismissInterceptors(entry);
             if (isDismissIntercepted(entry)) {
-                mLogger.logNotifDismissedIntercepted(entry.getKey());
+                mLogger.logNotifDismissedIntercepted(entry);
                 continue;
             }
 
@@ -299,7 +304,7 @@
                             stats.notificationVisibility);
                 } catch (RemoteException e) {
                     // system process is dead if we're here.
-                    mLogger.logRemoteExceptionOnNotificationClear(entry.getKey(), e);
+                    mLogger.logRemoteExceptionOnNotificationClear(entry, e);
                 }
             }
         }
@@ -342,7 +347,7 @@
                 // interceptors the chance to filter the notification
                 updateDismissInterceptors(entry);
                 if (isDismissIntercepted(entry)) {
-                    mLogger.logNotifClearAllDismissalIntercepted(entry.getKey());
+                    mLogger.logNotifClearAllDismissalIntercepted(entry);
                 }
                 entries.remove(i);
             }
@@ -363,7 +368,7 @@
             NotificationEntry entry = entries.get(i);
 
             entry.setDismissState(DISMISSED);
-            mLogger.logNotifDismissed(entry.getKey());
+            mLogger.logNotifDismissed(entry);
 
             if (isCanceled(entry)) {
                 canceledEntries.add(entry);
@@ -416,12 +421,12 @@
             int reason) {
         Assert.isMainThread();
 
-        mLogger.logNotifRemoved(sbn.getKey(), reason);
+        mLogger.logNotifRemoved(sbn, reason);
 
         final NotificationEntry entry = mNotificationSet.get(sbn.getKey());
         if (entry == null) {
             // TODO (b/160008901): Throw an exception here
-            mLogger.logNoNotificationToRemoveWithKey(sbn.getKey(), reason);
+            mLogger.logNoNotificationToRemoveWithKey(sbn, reason);
             return;
         }
 
@@ -464,7 +469,7 @@
             mEventQueue.add(new BindEntryEvent(entry, sbn));
             mNotificationSet.put(sbn.getKey(), entry);
 
-            mLogger.logNotifPosted(sbn.getKey());
+            mLogger.logNotifPosted(entry);
             mEventQueue.add(new EntryAddedEvent(entry));
 
         } else {
@@ -483,7 +488,7 @@
             entry.setSbn(sbn);
             mEventQueue.add(new BindEntryEvent(entry, sbn));
 
-            mLogger.logNotifUpdated(sbn.getKey());
+            mLogger.logNotifUpdated(entry);
             mEventQueue.add(new EntryUpdatedEvent(entry, true /* fromSystem */));
         }
     }
@@ -498,12 +503,12 @@
         if (mNotificationSet.get(entry.getKey()) != entry) {
             throw mEulogizer.record(
                     new IllegalStateException("No notification to remove with key "
-                            + entry.getKey()));
+                            + logKey(entry)));
         }
 
         if (!isCanceled(entry)) {
             throw mEulogizer.record(
-                    new IllegalStateException("Cannot remove notification " + entry.getKey()
+                    new IllegalStateException("Cannot remove notification " + logKey(entry)
                             + ": has not been marked for removal"));
         }
 
@@ -514,7 +519,7 @@
         }
 
         if (!isLifetimeExtended(entry)) {
-            mLogger.logNotifReleased(entry.getKey());
+            mLogger.logNotifReleased(entry);
             mNotificationSet.remove(entry.getKey());
             cancelDismissInterception(entry);
             mEventQueue.add(new EntryRemovedEvent(entry, entry.mCancellationReason));
@@ -559,6 +564,7 @@
     }
 
     private void applyRanking(@NonNull RankingMap rankingMap) {
+        ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null;
         for (NotificationEntry entry : mNotificationSet.values()) {
             if (!isCanceled(entry)) {
 
@@ -580,10 +586,27 @@
                         }
                     }
                 } else {
-                    mLogger.logRankingMissing(entry.getKey(), rankingMap);
+                    if (currentEntriesWithoutRankings == null) {
+                        currentEntriesWithoutRankings = new ArrayMap<>();
+                    }
+                    currentEntriesWithoutRankings.put(entry.getKey(), entry);
                 }
             }
         }
+        NotifCollectionLoggerKt.maybeLogInconsistentRankings(
+                mLogger,
+                mNotificationsWithoutRankings,
+                currentEntriesWithoutRankings,
+                rankingMap
+        );
+        mNotificationsWithoutRankings = currentEntriesWithoutRankings == null
+                ? Collections.emptySet() : currentEntriesWithoutRankings.keySet();
+        if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
+            for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
+                entry.mCancellationReason = REASON_UNKNOWN;
+                tryRemoveNotification(entry);
+            }
+        }
         mEventQueue.add(new RankingAppliedEvent());
     }
 
@@ -627,10 +650,7 @@
                             extender.getName(), logKey, collectionEntryIs)));
         }
 
-        mLogger.logLifetimeExtensionEnded(
-                entry.getKey(),
-                extender,
-                entry.mLifetimeExtenders.size());
+        mLogger.logLifetimeExtensionEnded(entry, extender, entry.mLifetimeExtenders.size());
 
         if (!isLifetimeExtended(entry)) {
             if (tryRemoveNotification(entry)) {
@@ -657,7 +677,7 @@
         mAmDispatchingToOtherCode = true;
         for (NotifLifetimeExtender extender : mLifetimeExtenders) {
             if (extender.maybeExtendLifetime(entry, entry.mCancellationReason)) {
-                mLogger.logLifetimeExtended(entry.getKey(), extender);
+                mLogger.logLifetimeExtended(entry, extender);
                 entry.mLifetimeExtenders.add(extender);
             }
         }
@@ -838,6 +858,11 @@
                         entries,
                         true,
                         "\t\t"));
+
+        pw.println("\n\tmNotificationsWithoutRankings: " + mNotificationsWithoutRankings.size());
+        for (String key : mNotificationsWithoutRankings) {
+            pw.println("\t * : " + key);
+        }
     }
 
     private final BatchableNotificationHandler mNotifHandler = new BatchableNotificationHandler() {
@@ -916,17 +941,17 @@
         // Make sure we have the notification to update
         NotificationEntry entry = mNotificationSet.get(sbn.getKey());
         if (entry == null) {
-            mLogger.logNotifInternalUpdateFailed(sbn.getKey(), name, reason);
+            mLogger.logNotifInternalUpdateFailed(sbn, name, reason);
             return;
         }
-        mLogger.logNotifInternalUpdate(sbn.getKey(), name, reason);
+        mLogger.logNotifInternalUpdate(entry, name, reason);
 
         // First do the pieces of postNotification which are not about assuming the notification
         // was sent by the app
         entry.setSbn(sbn);
         mEventQueue.add(new BindEntryEvent(entry, sbn));
 
-        mLogger.logNotifUpdated(sbn.getKey());
+        mLogger.logNotifUpdated(entry);
         mEventQueue.add(new EntryUpdatedEvent(entry, false /* fromSystem */));
 
         // Skip the applyRanking step and go straight to dispatching the events
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index aedbd1b..0a16fb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -175,6 +175,10 @@
     public boolean mRemoteEditImeAnimatingAway;
     public boolean mRemoteEditImeVisible;
     private boolean mExpandAnimationRunning;
+    /**
+     * Flag to determine if the entry is blockable by DnD filters
+     */
+    private boolean mBlockable;
 
     /**
      * @param sbn the StatusBarNotification from system server
@@ -253,6 +257,7 @@
         }
 
         mRanking = ranking.withAudiblyAlertedInfo(mRanking);
+        updateIsBlockable();
     }
 
     /*
@@ -781,15 +786,20 @@
      * or is not in an allowList).
      */
     public boolean isBlockable() {
+        return mBlockable;
+    }
+
+    private void updateIsBlockable() {
         if (getChannel() == null) {
-            return false;
+            mBlockable = false;
+            return;
         }
         if (getChannel().isImportanceLockedByCriticalDeviceFunction()
                 && !getChannel().isBlockable()) {
-            return false;
+            mBlockable = false;
+            return;
         }
-
-        return true;
+        mBlockable = true;
     }
 
     private boolean shouldSuppressVisualEffect(int effect) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index df2fe4e..6441d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -579,11 +579,7 @@
                     if (existingSummary == null) {
                         group.setSummary(entry);
                     } else {
-                        mLogger.logDuplicateSummary(
-                                mIterationCount,
-                                group.getKey(),
-                                existingSummary.getKey(),
-                                entry.getKey());
+                        mLogger.logDuplicateSummary(mIterationCount, group, existingSummary, entry);
 
                         // Use whichever one was posted most recently
                         if (entry.getSbn().getPostTime()
@@ -990,7 +986,7 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isSorted(mNotifList, mTopLevelComparator);
+            boolean isSorted = isShadeSorted();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
@@ -999,9 +995,23 @@
         Trace.endSection();
     }
 
+    private boolean isShadeSorted() {
+        if (!isSorted(mNotifList, mTopLevelComparator)) {
+            return false;
+        }
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
-    public static <T> boolean isSorted(List<T> items, Comparator<T> comparator) {
+    public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
         if (items.size() <= 1) {
             return true;
         }
@@ -1070,7 +1080,7 @@
         if (!Objects.equals(curr, prev)) {
             mLogger.logEntryAttachStateChanged(
                     mIterationCount,
-                    entry.getKey(),
+                    entry,
                     prev.getParent(),
                     curr.getParent());
 
@@ -1209,7 +1219,7 @@
     };
 
 
-    private final Comparator<ListEntry> mGroupChildrenComparator = (o1, o2) -> {
+    private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
         int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex();
         int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex();
         int cmp = Integer.compare(index1, index2);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
index ff1c70c..ef63be0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.java
@@ -16,15 +16,15 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator;
 
-import com.android.keyguard.KeyguardUpdateMonitor;
+import androidx.annotation.NonNull;
+
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 
 import javax.inject.Inject;
@@ -36,27 +36,21 @@
 @CoordinatorScope
 public class KeyguardCoordinator implements Coordinator {
     private static final String TAG = "KeyguardCoordinator";
-    private final StatusBarStateController mStatusBarStateController;
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final HighPriorityProvider mHighPriorityProvider;
-    private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
     private final KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
+    private final SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
     private final SharedCoordinatorLogger mLogger;
+    private final StatusBarStateController mStatusBarStateController;
 
     @Inject
     public KeyguardCoordinator(
-            StatusBarStateController statusBarStateController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            HighPriorityProvider highPriorityProvider,
-            SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
             KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            SharedCoordinatorLogger logger) {
-        mStatusBarStateController = statusBarStateController;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mHighPriorityProvider = highPriorityProvider;
-        mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
+            SectionHeaderVisibilityProvider sectionHeaderVisibilityProvider,
+            SharedCoordinatorLogger logger,
+            StatusBarStateController statusBarStateController) {
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
+        mSectionHeaderVisibilityProvider = sectionHeaderVisibilityProvider;
         mLogger = logger;
+        mStatusBarStateController = statusBarStateController;
     }
 
     @Override
@@ -72,7 +66,7 @@
 
     private final NotifFilter mNotifFilter = new NotifFilter(TAG) {
         @Override
-        public boolean shouldFilterOut(NotificationEntry entry, long now) {
+        public boolean shouldFilterOut(@NonNull NotificationEntry entry, long now) {
             return mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry);
         }
     };
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 8f37baf..023c4ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -360,13 +360,13 @@
     }
 
     private void abortInflation(NotificationEntry entry, String reason) {
-        mLogger.logInflationAborted(entry.getKey(), reason);
+        mLogger.logInflationAborted(entry, reason);
         mNotifInflater.abortInflation(entry);
         mInflatingNotifs.remove(entry);
     }
 
     private void onInflationFinished(NotificationEntry entry, NotifViewController controller) {
-        mLogger.logNotifInflated(entry.getKey());
+        mLogger.logNotifInflated(entry);
         mInflatingNotifs.remove(entry);
         mViewBarn.registerViewForEntry(entry, controller);
         mInflationStates.put(entry, STATE_INFLATED);
@@ -398,20 +398,20 @@
             return false;
         }
         if (isBeyondGroupInitializationWindow(group, now)) {
-            mLogger.logGroupInflationTookTooLong(group.getKey());
+            mLogger.logGroupInflationTookTooLong(group);
             return false;
         }
         if (mInflatingNotifs.contains(group.getSummary())) {
-            mLogger.logDelayingGroupRelease(group.getKey(), group.getSummary().getKey());
+            mLogger.logDelayingGroupRelease(group, group.getSummary());
             return true;
         }
         for (NotificationEntry child : group.getChildren()) {
             if (mInflatingNotifs.contains(child) && !child.wasAttachedInPreviousPass()) {
-                mLogger.logDelayingGroupRelease(group.getKey(), child.getKey());
+                mLogger.logDelayingGroupRelease(group, child);
                 return true;
             }
         }
-        mLogger.logDoneWaitingForGroupInflation(group.getKey());
+        mLogger.logDoneWaitingForGroupInflation(group);
         return false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index f835250..30f1315 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -19,48 +19,51 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class PreparationCoordinatorLogger @Inject constructor(
     @NotificationLog private val buffer: LogBuffer
 ) {
-    fun logNotifInflated(key: String) {
+    fun logNotifInflated(entry: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "NOTIF INFLATED $str1"
         })
     }
 
-    fun logInflationAborted(key: String, reason: String) {
+    fun logInflationAborted(entry: NotificationEntry, reason: String) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = key
+            str1 = entry.logKey
             str2 = reason
         }, {
             "NOTIF INFLATION ABORTED $str1 reason=$str2"
         })
     }
 
-    fun logDoneWaitingForGroupInflation(groupKey: String) {
+    fun logDoneWaitingForGroupInflation(group: GroupEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = groupKey
+            str1 = group.logKey
         }, {
             "Finished inflating all members of group $str1, releasing group"
         })
     }
 
-    fun logGroupInflationTookTooLong(groupKey: String) {
+    fun logGroupInflationTookTooLong(group: GroupEntry) {
         buffer.log(TAG, LogLevel.WARNING, {
-            str1 = groupKey
+            str1 = group.logKey
         }, {
             "Group inflation took too long for $str1, releasing children early"
         })
     }
 
-    fun logDelayingGroupRelease(groupKey: String, childKey: String) {
+    fun logDelayingGroupRelease(group: GroupEntry, child: NotificationEntry) {
         buffer.log(TAG, LogLevel.DEBUG, {
-            str1 = groupKey
-            str2 = childKey
+            str1 = group.logKey
+            str2 = child.logKey
         }, {
             "Delaying release of group $str1 because child $str2 is still inflating"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index 57fd197..5ac4813 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -20,7 +20,6 @@
 import android.annotation.Nullable;
 
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.notification.SectionClassifier;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -28,6 +27,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
 import com.android.systemui.statusbar.notification.collection.render.NodeController;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.AlertingHeader;
@@ -51,7 +51,7 @@
     public static final boolean SHOW_ALL_SECTIONS = false;
     private final StatusBarStateController mStatusBarStateController;
     private final HighPriorityProvider mHighPriorityProvider;
-    private final SectionClassifier mSectionClassifier;
+    private final SectionStyleProvider mSectionStyleProvider;
     private final NodeController mSilentNodeController;
     private final SectionHeaderController mSilentHeaderController;
     private final NodeController mAlertingHeaderController;
@@ -62,13 +62,13 @@
     public RankingCoordinator(
             StatusBarStateController statusBarStateController,
             HighPriorityProvider highPriorityProvider,
-            SectionClassifier sectionClassifier,
+            SectionStyleProvider sectionStyleProvider,
             @AlertingHeader NodeController alertingHeaderController,
             @SilentHeader SectionHeaderController silentHeaderController,
             @SilentHeader NodeController silentNodeController) {
         mStatusBarStateController = statusBarStateController;
         mHighPriorityProvider = highPriorityProvider;
-        mSectionClassifier = sectionClassifier;
+        mSectionStyleProvider = sectionStyleProvider;
         mAlertingHeaderController = alertingHeaderController;
         mSilentNodeController = silentNodeController;
         mSilentHeaderController = silentHeaderController;
@@ -77,7 +77,7 @@
     @Override
     public void attach(NotifPipeline pipeline) {
         mStatusBarStateController.addCallback(mStatusBarStateCallback);
-        mSectionClassifier.setMinimizedSections(Collections.singleton(mMinimizedNotifSectioner));
+        mSectionStyleProvider.setMinimizedSections(Collections.singleton(mMinimizedNotifSectioner));
 
         pipeline.addPreGroupFilter(mSuspendedFilter);
         pipeline.addPreGroupFilter(mDndVisualEffectsFilter);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
index 4e9d3ac..1494574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinator.kt
@@ -19,11 +19,11 @@
 import android.content.Context
 import com.android.systemui.R
 import com.android.systemui.statusbar.notification.AssistantFeedbackController
-import com.android.systemui.statusbar.notification.SectionClassifier
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.statusbar.notification.collection.render.NotifRowController
 import javax.inject.Inject
 
@@ -35,7 +35,7 @@
 class RowAppearanceCoordinator @Inject internal constructor(
     context: Context,
     private var mAssistantFeedbackController: AssistantFeedbackController,
-    private var mSectionClassifier: SectionClassifier
+    private var mSectionStyleProvider: SectionStyleProvider
 ) : Coordinator {
 
     private var entryToExpand: NotificationEntry? = null
@@ -55,7 +55,7 @@
 
     private fun onBeforeRenderList(list: List<ListEntry>) {
         entryToExpand = list.firstOrNull()?.representativeEntry?.takeIf { entry ->
-            !mSectionClassifier.isMinimizedSection(entry.section!!)
+            !mSectionStyleProvider.isMinimizedSection(entry.section!!)
         }
     }
 
@@ -68,4 +68,4 @@
         // Show the "alerted" bell icon
         controller.setLastAudiblyAlertedMs(entry.lastAudiblyAlertedMs)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
index 3475fcf..ee0b008 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
@@ -31,6 +31,7 @@
     val smartActions: List<Notification.Action>,
     val smartReplies: List<CharSequence>,
     val isConversation: Boolean,
+    val isSnoozeEnabled: Boolean,
     val isMinimized: Boolean,
     val needsRedaction: Boolean,
 ) {
@@ -42,6 +43,7 @@
         ): Boolean = when {
             oldAdjustment === newAdjustment -> false
             oldAdjustment.isConversation != newAdjustment.isConversation -> true
+            oldAdjustment.isSnoozeEnabled != newAdjustment.isSnoozeEnabled -> true
             oldAdjustment.isMinimized != newAdjustment.isMinimized -> true
             oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true
             areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true
@@ -57,9 +59,9 @@
             first.size != second.size -> true
             else -> first.asSequence().zip(second.asSequence()).any {
                 (!TextUtils.equals(it.first.title, it.second.title)) ||
-                        (areDifferent(it.first.getIcon(), it.second.getIcon())) ||
-                        (it.first.actionIntent != it.second.actionIntent) ||
-                        (areDifferent(it.first.remoteInputs, it.second.remoteInputs))
+                    (areDifferent(it.first.getIcon(), it.second.getIcon())) ||
+                    (it.first.actionIntent != it.second.actionIntent) ||
+                    (areDifferent(it.first.remoteInputs, it.second.remoteInputs))
             }
         }
 
@@ -78,7 +80,7 @@
             first.size != second.size -> true
             else -> first.asSequence().zip(second.asSequence()).any {
                 (!TextUtils.equals(it.first.label, it.second.label)) ||
-                        (areDifferent(it.first.choices, it.second.choices))
+                    (areDifferent(it.first.choices, it.second.choices))
             }
         }
 
@@ -94,4 +96,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
index f7b6376..745d6fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
@@ -16,12 +16,18 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation
 
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.UserHandle
+import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.notification.SectionClassifier
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.util.ListenerSet
+import com.android.systemui.util.settings.SecureSettings
 import javax.inject.Inject
 
 /**
@@ -30,14 +36,23 @@
  */
 @SysUISingleton
 class NotifUiAdjustmentProvider @Inject constructor(
+    @Main private val handler: Handler,
+    private val secureSettings: SecureSettings,
     private val lockscreenUserManager: NotificationLockscreenUserManager,
-    private val sectionClassifier: SectionClassifier,
+    private val sectionStyleProvider: SectionStyleProvider
 ) {
     private val dirtyListeners = ListenerSet<Runnable>()
+    private var isSnoozeEnabled = false
 
     fun addDirtyListener(listener: Runnable) {
         if (dirtyListeners.isEmpty()) {
             lockscreenUserManager.addNotificationStateChangedListener(notifStateChangedListener)
+            updateSnoozeEnabled()
+            secureSettings.registerContentObserverForUser(
+                SHOW_NOTIFICATION_SNOOZE,
+                settingsObserver,
+                UserHandle.USER_ALL
+            )
         }
         dirtyListeners.addIfAbsent(listener)
     }
@@ -46,6 +61,7 @@
         dirtyListeners.remove(listener)
         if (dirtyListeners.isEmpty()) {
             lockscreenUserManager.removeNotificationStateChangedListener(notifStateChangedListener)
+            secureSettings.unregisterContentObserver(settingsObserver)
         }
     }
 
@@ -54,10 +70,21 @@
             dirtyListeners.forEach(Runnable::run)
         }
 
+    private val settingsObserver = object : ContentObserver(handler) {
+        override fun onChange(selfChange: Boolean) {
+            updateSnoozeEnabled()
+            dirtyListeners.forEach(Runnable::run)
+        }
+    }
+
+    private fun updateSnoozeEnabled() {
+        isSnoozeEnabled = secureSettings.getInt(SHOW_NOTIFICATION_SNOOZE, 0) == 1
+    }
+
     private fun isEntryMinimized(entry: NotificationEntry): Boolean {
         val section = entry.section ?: error("Entry must have a section to determine if minimized")
         val parent = entry.parent ?: error("Entry must have a parent to determine if minimized")
-        val isMinimizedSection = sectionClassifier.isMinimizedSection(section)
+        val isMinimizedSection = sectionStyleProvider.isMinimizedSection(section)
         val isTopLevelEntry = parent == GroupEntry.ROOT_ENTRY
         val isGroupSummary = parent.summary == entry
         return isMinimizedSection && (isTopLevelEntry || isGroupSummary)
@@ -73,7 +100,8 @@
         smartActions = entry.ranking.smartActions,
         smartReplies = entry.ranking.smartReplies,
         isConversation = entry.ranking.isConversation,
+        isSnoozeEnabled = isSnoozeEnabled,
         isMinimized = isEntryMinimized(entry),
         needsRedaction = lockscreenUserManager.needsRedaction(entry),
     )
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index f8bf85f..8d1759b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -23,8 +23,10 @@
 import com.android.systemui.log.dagger.NotificationLog
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class ShadeListBuilderLogger @Inject constructor(
@@ -110,12 +112,17 @@
         })
     }
 
-    fun logDuplicateSummary(buildId: Int, groupKey: String, existingKey: String, newKey: String) {
+    fun logDuplicateSummary(
+        buildId: Int,
+        group: GroupEntry,
+        existingSummary: NotificationEntry,
+        newSummary: NotificationEntry
+    ) {
         buffer.log(TAG, WARNING, {
             long1 = buildId.toLong()
-            str1 = groupKey
-            str2 = existingKey
-            str3 = newKey
+            str1 = group.logKey
+            str2 = existingSummary.logKey
+            str3 = newSummary.logKey
         }, {
             """(Build $long1) Duplicate summary for group "$str1": "$str2" vs. "$str3""""
         })
@@ -124,7 +131,7 @@
     fun logDuplicateTopLevelKey(buildId: Int, topLevelKey: String) {
         buffer.log(TAG, WARNING, {
             long1 = buildId.toLong()
-            str1 = topLevelKey
+            str1 = logKey(topLevelKey)
         }, {
             "(Build $long1) Duplicate top-level key: $str1"
         })
@@ -132,15 +139,15 @@
 
     fun logEntryAttachStateChanged(
         buildId: Int,
-        key: String,
+        entry: ListEntry,
         prevParent: GroupEntry?,
         newParent: GroupEntry?
     ) {
         buffer.log(TAG, INFO, {
             long1 = buildId.toLong()
-            str1 = key
-            str2 = prevParent?.key
-            str3 = newParent?.key
+            str1 = entry.logKey
+            str2 = prevParent?.logKey
+            str3 = newParent?.logKey
         }, {
 
             val action = if (str2 == null && str3 != null) {
@@ -160,8 +167,8 @@
     fun logParentChanged(buildId: Int, prevParent: GroupEntry?, newParent: GroupEntry?) {
         buffer.log(TAG, INFO, {
             long1 = buildId.toLong()
-            str1 = prevParent?.key
-            str2 = newParent?.key
+            str1 = prevParent?.logKey
+            str2 = newParent?.logKey
         }, {
             if (str1 == null && str2 != null) {
                 "(Build $long1)     Parent is {$str2}"
@@ -180,8 +187,8 @@
     ) {
         buffer.log(TAG, INFO, {
             long1 = buildId.toLong()
-            str1 = suppressedParent?.key
-            str2 = keepingParent?.key
+            str1 = suppressedParent?.logKey
+            str2 = keepingParent?.logKey
         }, {
             "(Build $long1)     Change of parent to '$str1' suppressed; keeping parent '$str2'"
         })
@@ -193,7 +200,7 @@
     ) {
         buffer.log(TAG, INFO, {
             long1 = buildId.toLong()
-            str1 = keepingParent?.key
+            str1 = keepingParent?.logKey
         }, {
             "(Build $long1)     Group pruning suppressed; keeping parent '$str1'"
         })
@@ -281,7 +288,7 @@
             val entry = entries[i]
             buffer.log(TAG, DEBUG, {
                 int1 = i
-                str1 = entry.key
+                str1 = entry.logKey
             }, {
                 "[$int1] $str1"
             })
@@ -289,7 +296,7 @@
             if (entry is GroupEntry) {
                 entry.summary?.let {
                     buffer.log(TAG, DEBUG, {
-                        str1 = it.key
+                        str1 = it.logKey
                     }, {
                         "  [*] $str1 (summary)"
                     })
@@ -298,7 +305,7 @@
                     val child = entry.children[j]
                     buffer.log(TAG, DEBUG, {
                         int1 = j
-                        str1 = child.key
+                        str1 = child.logKey
                     }, {
                         "  [$int1] $str1"
                     })
@@ -308,7 +315,7 @@
     }
 
     fun logPipelineRunSuppressed() =
-            buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." }
+        buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." }
 }
 
 private const val TAG = "ShadeListBuilder"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index ac0b1ee..ebcac6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -19,6 +19,7 @@
 import android.os.RemoteException
 import android.service.notification.NotificationListenerService
 import android.service.notification.NotificationListenerService.RankingMap
+import android.service.notification.StatusBarNotification
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.LogLevel.ERROR
@@ -65,9 +66,9 @@
 class NotifCollectionLogger @Inject constructor(
     @NotificationLog private val buffer: LogBuffer
 ) {
-    fun logNotifPosted(key: String) {
+    fun logNotifPosted(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "POSTED $str1"
         })
@@ -75,49 +76,49 @@
 
     fun logNotifGroupPosted(groupKey: String, batchSize: Int) {
         buffer.log(TAG, INFO, {
-            str1 = groupKey
+            str1 = logKey(groupKey)
             int1 = batchSize
         }, {
             "POSTED GROUP $str1 ($int1 events)"
         })
     }
 
-    fun logNotifUpdated(key: String) {
+    fun logNotifUpdated(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "UPDATED $str1"
         })
     }
 
-    fun logNotifRemoved(key: String, @CancellationReason reason: Int) {
+    fun logNotifRemoved(sbn: StatusBarNotification, @CancellationReason reason: Int) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = sbn.logKey
             int1 = reason
         }, {
             "REMOVED $str1 reason=${cancellationReasonDebugString(int1)}"
         })
     }
 
-    fun logNotifReleased(key: String) {
+    fun logNotifReleased(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "RELEASED $str1"
         })
     }
 
-    fun logNotifDismissed(key: String) {
+    fun logNotifDismissed(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "DISMISSED $str1"
         })
     }
 
-    fun logNonExistentNotifDismissed(key: String) {
+    fun logNonExistentNotifDismissed(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "DISMISSED Non Existent $str1"
         })
@@ -125,7 +126,7 @@
 
     fun logChildDismissed(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "CHILD DISMISSED (inferred): $str1"
         })
@@ -141,31 +142,31 @@
 
     fun logDismissOnAlreadyCanceledEntry(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = entry.key
+            str1 = entry.logKey
         }, {
             "Dismiss on $str1, which was already canceled. Trying to remove..."
         })
     }
 
-    fun logNotifDismissedIntercepted(key: String) {
+    fun logNotifDismissedIntercepted(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "DISMISS INTERCEPTED $str1"
         })
     }
 
-    fun logNotifClearAllDismissalIntercepted(key: String) {
+    fun logNotifClearAllDismissalIntercepted(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "CLEAR ALL DISMISSAL INTERCEPTED $str1"
         })
     }
 
-    fun logNotifInternalUpdate(key: String, name: String, reason: String) {
+    fun logNotifInternalUpdate(entry: NotificationEntry, name: String, reason: String) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = name
             str3 = reason
         }, {
@@ -173,9 +174,9 @@
         })
     }
 
-    fun logNotifInternalUpdateFailed(key: String, name: String, reason: String) {
+    fun logNotifInternalUpdateFailed(sbn: StatusBarNotification, name: String, reason: String) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = sbn.logKey
             str2 = name
             str3 = reason
         }, {
@@ -183,26 +184,49 @@
         })
     }
 
-    fun logNoNotificationToRemoveWithKey(key: String, @CancellationReason reason: Int) {
+    fun logNoNotificationToRemoveWithKey(
+        sbn: StatusBarNotification,
+        @CancellationReason reason: Int
+    ) {
         buffer.log(TAG, ERROR, {
-            str1 = key
+            str1 = sbn.logKey
             int1 = reason
         }, {
             "No notification to remove with key $str1 reason=${cancellationReasonDebugString(int1)}"
         })
     }
 
-    fun logRankingMissing(key: String, rankingMap: RankingMap) {
-        buffer.log(TAG, WARNING, { str1 = key }, { "Ranking update is missing ranking for $str1" })
-        buffer.log(TAG, DEBUG, {}, { "Ranking map contents:" })
-        for (entry in rankingMap.orderedKeys) {
-            buffer.log(TAG, DEBUG, { str1 = entry }, { "  $str1" })
-        }
+    fun logMissingRankings(
+        newlyInconsistentEntries: List<NotificationEntry>,
+        totalInconsistent: Int,
+        rankingMap: RankingMap
+    ) {
+        buffer.log(TAG, WARNING, {
+            int1 = totalInconsistent
+            int2 = newlyInconsistentEntries.size
+            str1 = newlyInconsistentEntries.joinToString { it.logKey ?: "null" }
+        }, {
+            "Ranking update is missing ranking for $int1 entries ($int2 new): $str1"
+        })
+        buffer.log(TAG, DEBUG, {
+            str1 = rankingMap.orderedKeys.map { logKey(it) ?: "null" }.toString()
+        }, {
+            "Ranking map contents: $str1"
+        })
     }
 
-    fun logRemoteExceptionOnNotificationClear(key: String, e: RemoteException) {
+    fun logRecoveredRankings(newlyConsistentKeys: List<String>) {
+        buffer.log(TAG, INFO, {
+            int1 = newlyConsistentKeys.size
+            str1 = newlyConsistentKeys.joinToString { logKey(it) ?: "null" }
+        }, {
+            "Ranking update now contains rankings for $int1 previously inconsistent entries: $str1"
+        })
+    }
+
+    fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) {
         buffer.log(TAG, WTF, {
-            str1 = key
+            str1 = entry.logKey
             str2 = e.toString()
         }, {
             "RemoteException while attempting to clear $str1:\n$str2"
@@ -217,9 +241,9 @@
         })
     }
 
-    fun logLifetimeExtended(key: String, extender: NotifLifetimeExtender) {
+    fun logLifetimeExtended(entry: NotificationEntry, extender: NotifLifetimeExtender) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = extender.name
         }, {
             "LIFETIME EXTENDED: $str1 by $str2"
@@ -227,12 +251,12 @@
     }
 
     fun logLifetimeExtensionEnded(
-        key: String,
+        entry: NotificationEntry,
         extender: NotifLifetimeExtender,
         totalExtenders: Int
     ) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = extender.name
             int1 = totalExtenders
         }, {
@@ -338,4 +362,29 @@
     }
 }
 
+fun maybeLogInconsistentRankings(
+    logger: NotifCollectionLogger,
+    oldKeysWithoutRankings: Set<String>,
+    newEntriesWithoutRankings: Map<String, NotificationEntry>?,
+    rankingMap: RankingMap
+) {
+    if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings == null) return
+    val newlyConsistent: List<String> = oldKeysWithoutRankings
+        .mapNotNull { key ->
+            key.takeIf { key !in (newEntriesWithoutRankings ?: emptyMap()) }
+                .takeIf { key in rankingMap.orderedKeys }
+        }.sorted()
+    if (newlyConsistent.isNotEmpty()) {
+        logger.logRecoveredRankings(newlyConsistent)
+    }
+    val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
+        ?.mapNotNull { (key, entry) ->
+            entry.takeIf { key !in oldKeysWithoutRankings }
+        }?.sortedBy { it.key } ?: emptyList()
+    if (newlyInconsistent.isNotEmpty()) {
+        val totalInconsistent: Int = newEntriesWithoutRankings?.size ?: 0
+        logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
+    }
+}
+
 private const val TAG = "NotifCollection"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionHeaderVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionHeaderVisibilityProvider.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
index 68bdd18..82c7aae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionHeaderVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionHeaderVisibilityProvider.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification
+package com.android.systemui.statusbar.notification.collection.provider
 
 import android.content.Context
-import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 
 /**
@@ -34,7 +34,7 @@
 class SectionHeaderVisibilityProvider @Inject constructor(
     context: Context
 ) {
-    var neverShowSectionHeaders = context.resources.getBoolean(R.bool.config_notification_never_show_section_headers)
-        private set
+    val neverShowSectionHeaders =
+        context.resources.getBoolean(R.bool.config_notification_never_show_section_headers)
     var sectionHeadersVisible = true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionClassifier.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionClassifier.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
index 1f2d0fe..7b94830 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/SectionClassifier.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/SectionStyleProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.notification
+package com.android.systemui.statusbar.notification.collection.provider
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
@@ -26,7 +26,7 @@
  * NOTE: This class exists to avoid putting metadata like "isMinimized" on the NotifSection
  */
 @SysUISingleton
-class SectionClassifier @Inject constructor() {
+class SectionStyleProvider @Inject constructor() {
     private lateinit var lowPrioritySections: Set<NotifSectioner>
 
     /**
@@ -43,4 +43,4 @@
     fun isMinimizedSection(section: NotifSection): Boolean {
         return lowPrioritySections.contains(section.sectioner)
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
index 8be710c..d234e54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilder.kt
@@ -16,12 +16,12 @@
 
 package com.android.systemui.statusbar.notification.collection.render
 
-import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.util.Compile
 import com.android.systemui.util.traceSection
 
@@ -107,4 +107,4 @@
                 .apply { entry.children.forEach { children.add(buildNotifNode(this, it)) } }
         else -> throw RuntimeException("Unexpected entry: $entry")
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
index 3501b44..38e3d49 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderLogger.kt
@@ -19,20 +19,24 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.util.Compile
 import javax.inject.Inject
 
 class NodeSpecBuilderLogger @Inject constructor(
+    notifPipelineFlags: NotifPipelineFlags,
     @NotificationLog private val buffer: LogBuffer
 ) {
+    private val devLoggingEnabled by lazy { notifPipelineFlags.isDevLoggingEnabled() }
+
     fun logBuildNodeSpec(
         oldSections: Set<NotifSection?>,
         newHeaders: Map<NotifSection?, NodeController?>,
         newCounts: Map<NotifSection?, Int>,
         newSectionOrder: List<NotifSection?>
     ) {
-        if (!Compile.IS_DEBUG)
+        if (!(Compile.IS_DEBUG && devLoggingEnabled))
             return
 
         buffer.log(TAG, LogLevel.DEBUG, {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
index 6ed8107..51dc728 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewManager.kt
@@ -19,10 +19,10 @@
 import android.content.Context
 import android.view.View
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
-import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer
 import com.android.systemui.util.traceSection
 import dagger.assisted.Assisted
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
index 19cf9dc..5ef2b9e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinder.java
@@ -83,7 +83,7 @@
         params.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
         params.requireContentViews(FLAG_CONTENT_VIEW_HEADS_UP);
         CancellationSignal signal = mStage.requestRebind(entry, en -> {
-            mLogger.entryBoundSuccessfully(entry.getKey());
+            mLogger.entryBoundSuccessfully(entry);
             en.getRow().setUsesIncreasedHeadsUpHeight(params.useIncreasedHeadsUpHeight());
             // requestRebing promises that if we called cancel before this callback would be
             // invoked, then we will not enter this callback, and because we always cancel before
@@ -94,7 +94,7 @@
             }
         });
         abortBindCallback(entry);
-        mLogger.startBindingHun(entry.getKey());
+        mLogger.startBindingHun(entry);
         mOngoingBindCallbacks.put(entry, signal);
     }
 
@@ -105,7 +105,7 @@
     public void abortBindCallback(NotificationEntry entry) {
         CancellationSignal ongoingBindCallback = mOngoingBindCallbacks.remove(entry);
         if (ongoingBindCallback != null) {
-            mLogger.currentOngoingBindingAborted(entry.getKey());
+            mLogger.currentOngoingBindingAborted(entry);
             ongoingBindCallback.cancel();
         }
     }
@@ -116,7 +116,7 @@
     public void unbindHeadsUpView(NotificationEntry entry) {
         abortBindCallback(entry);
         mStage.getStageParams(entry).markContentViewsFreeable(FLAG_CONTENT_VIEW_HEADS_UP);
-        mLogger.entryContentViewMarkedFreeable(entry.getKey());
-        mStage.requestRebind(entry, e -> mLogger.entryUnbound(e.getKey()));
+        mLogger.entryContentViewMarkedFreeable(entry);
+        mStage.requestRebind(entry, e -> mLogger.entryUnbound(e));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
index 50a6207..d1feaa0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderLogger.kt
@@ -3,44 +3,46 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class HeadsUpViewBinderLogger @Inject constructor(@NotificationHeadsUpLog val buffer: LogBuffer) {
-    fun startBindingHun(key: String) {
+    fun startBindingHun(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "start binding heads up entry $str1 "
         })
     }
 
-    fun currentOngoingBindingAborted(key: String) {
+    fun currentOngoingBindingAborted(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "aborted potential ongoing heads up entry binding $str1 "
         })
     }
 
-    fun entryBoundSuccessfully(key: String) {
+    fun entryBoundSuccessfully(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "heads up entry bound successfully $str1 "
         })
     }
 
-    fun entryUnbound(key: String) {
+    fun entryUnbound(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "heads up entry unbound successfully $str1 "
         })
     }
 
-    fun entryContentViewMarkedFreeable(key: String) {
+    fun entryContentViewMarkedFreeable(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "start unbinding heads up entry $str1 "
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 1d18ca3..016b388 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -16,11 +16,12 @@
 
 package com.android.systemui.statusbar.notification.interruption
 
-import android.service.notification.StatusBarNotification
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.DEBUG
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationInterruptLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class NotificationInterruptLogger @Inject constructor(
@@ -41,17 +42,17 @@
         })
     }
 
-    fun logNoBubbleNotAllowed(sbn: StatusBarNotification) {
+    fun logNoBubbleNotAllowed(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No bubble up: not allowed to bubble: $str1"
         })
     }
 
-    fun logNoBubbleNoMetadata(sbn: StatusBarNotification) {
+    fun logNoBubbleNoMetadata(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No bubble up: notification: $str1 doesn't have valid metadata"
         })
@@ -64,89 +65,89 @@
         })
     }
 
-    fun logNoHeadsUpPackageSnoozed(sbn: StatusBarNotification) {
+    fun logNoHeadsUpPackageSnoozed(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No alerting: snoozed package: $str1"
         })
     }
 
-    fun logNoHeadsUpAlreadyBubbled(sbn: StatusBarNotification) {
+    fun logNoHeadsUpAlreadyBubbled(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No heads up: in unlocked shade where notification is shown as a bubble: $str1"
         })
     }
 
-    fun logNoHeadsUpSuppressedByDnd(sbn: StatusBarNotification) {
+    fun logNoHeadsUpSuppressedByDnd(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No heads up: suppressed by DND: $str1"
         })
     }
 
-    fun logNoHeadsUpNotImportant(sbn: StatusBarNotification) {
+    fun logNoHeadsUpNotImportant(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No heads up: unimportant notification: $str1"
         })
     }
 
-    fun logNoHeadsUpNotInUse(sbn: StatusBarNotification) {
+    fun logNoHeadsUpNotInUse(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No heads up: not in use: $str1"
         })
     }
 
     fun logNoHeadsUpSuppressedBy(
-        sbn: StatusBarNotification,
+        entry: NotificationEntry,
         suppressor: NotificationInterruptSuppressor
     ) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
             str2 = suppressor.name
         }, {
             "No heads up: aborted by suppressor: $str2 sbnKey=$str1"
         })
     }
 
-    fun logHeadsUp(sbn: StatusBarNotification) {
+    fun logHeadsUp(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "Heads up: $str1"
         })
     }
 
-    fun logNoAlertingFilteredOut(sbn: StatusBarNotification) {
+    fun logNoAlertingFilteredOut(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No alerting: filtered notification: $str1"
         })
     }
 
-    fun logNoAlertingGroupAlertBehavior(sbn: StatusBarNotification) {
+    fun logNoAlertingGroupAlertBehavior(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No alerting: suppressed due to group alert behavior: $str1"
         })
     }
 
     fun logNoAlertingSuppressedBy(
-        sbn: StatusBarNotification,
+        entry: NotificationEntry,
         suppressor: NotificationInterruptSuppressor,
         awake: Boolean
     ) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
             str2 = suppressor.name
             bool1 = awake
         }, {
@@ -154,65 +155,65 @@
         })
     }
 
-    fun logNoAlertingRecentFullscreen(sbn: StatusBarNotification) {
+    fun logNoAlertingRecentFullscreen(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No alerting: recent fullscreen: $str1"
         })
     }
 
-    fun logNoPulsingSettingDisabled(sbn: StatusBarNotification) {
+    fun logNoPulsingSettingDisabled(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No pulsing: disabled by setting: $str1"
         })
     }
 
-    fun logNoPulsingBatteryDisabled(sbn: StatusBarNotification) {
+    fun logNoPulsingBatteryDisabled(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No pulsing: disabled by battery saver: $str1"
         })
     }
 
-    fun logNoPulsingNoAlert(sbn: StatusBarNotification) {
+    fun logNoPulsingNoAlert(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No pulsing: notification shouldn't alert: $str1"
         })
     }
 
-    fun logNoPulsingNoAmbientEffect(sbn: StatusBarNotification) {
+    fun logNoPulsingNoAmbientEffect(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No pulsing: ambient effect suppressed: $str1"
         })
     }
 
-    fun logNoPulsingNotImportant(sbn: StatusBarNotification) {
+    fun logNoPulsingNotImportant(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "No pulsing: not important enough: $str1"
         })
     }
 
-    fun logPulsing(sbn: StatusBarNotification) {
+    fun logPulsing(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = sbn.key
+            str1 = entry.logKey
         }, {
             "Pulsing: $str1"
         })
     }
 
-    fun keyguardHideNotification(key: String) {
+    fun keyguardHideNotification(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "Keyguard Hide Notification: $str1"
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index a063dbd..8378b69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -147,14 +147,14 @@
         }
 
         if (!entry.canBubble()) {
-            mLogger.logNoBubbleNotAllowed(sbn);
+            mLogger.logNoBubbleNotAllowed(entry);
             return false;
         }
 
         if (entry.getBubbleMetadata() == null
                 || (entry.getBubbleMetadata().getShortcutId() == null
                     && entry.getBubbleMetadata().getIntent() == null)) {
-            mLogger.logNoBubbleNoMetadata(sbn);
+            mLogger.logNoBubbleNoMetadata(entry);
             return false;
         }
 
@@ -203,23 +203,23 @@
         }
 
         if (isSnoozedPackage(sbn)) {
-            mLogger.logNoHeadsUpPackageSnoozed(sbn);
+            mLogger.logNoHeadsUpPackageSnoozed(entry);
             return false;
         }
 
         boolean inShade = mStatusBarStateController.getState() == SHADE;
         if (entry.isBubble() && inShade) {
-            mLogger.logNoHeadsUpAlreadyBubbled(sbn);
+            mLogger.logNoHeadsUpAlreadyBubbled(entry);
             return false;
         }
 
         if (entry.shouldSuppressPeek()) {
-            mLogger.logNoHeadsUpSuppressedByDnd(sbn);
+            mLogger.logNoHeadsUpSuppressedByDnd(entry);
             return false;
         }
 
         if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) {
-            mLogger.logNoHeadsUpNotImportant(sbn);
+            mLogger.logNoHeadsUpNotImportant(entry);
             return false;
         }
 
@@ -232,17 +232,17 @@
         boolean inUse = mPowerManager.isScreenOn() && !isDreaming;
 
         if (!inUse) {
-            mLogger.logNoHeadsUpNotInUse(sbn);
+            mLogger.logNoHeadsUpNotInUse(entry);
             return false;
         }
 
         for (int i = 0; i < mSuppressors.size(); i++) {
             if (mSuppressors.get(i).suppressAwakeHeadsUp(entry)) {
-                mLogger.logNoHeadsUpSuppressedBy(sbn, mSuppressors.get(i));
+                mLogger.logNoHeadsUpSuppressedBy(entry, mSuppressors.get(i));
                 return false;
             }
         }
-        mLogger.logHeadsUp(sbn);
+        mLogger.logHeadsUp(entry);
         return true;
     }
 
@@ -254,38 +254,36 @@
      * @return true if the entry should ambient pulse, false otherwise
      */
     private boolean shouldHeadsUpWhenDozing(NotificationEntry entry) {
-        StatusBarNotification sbn = entry.getSbn();
-
         if (!mAmbientDisplayConfiguration.pulseOnNotificationEnabled(UserHandle.USER_CURRENT)) {
-            mLogger.logNoPulsingSettingDisabled(sbn);
+            mLogger.logNoPulsingSettingDisabled(entry);
             return false;
         }
 
         if (mBatteryController.isAodPowerSave()) {
-            mLogger.logNoPulsingBatteryDisabled(sbn);
+            mLogger.logNoPulsingBatteryDisabled(entry);
             return false;
         }
 
         if (!canAlertCommon(entry)) {
-            mLogger.logNoPulsingNoAlert(sbn);
+            mLogger.logNoPulsingNoAlert(entry);
             return false;
         }
 
         if (!canAlertHeadsUpCommon(entry)) {
-            mLogger.logNoPulsingNoAlert(sbn);
+            mLogger.logNoPulsingNoAlert(entry);
             return false;
         }
 
         if (entry.shouldSuppressAmbient()) {
-            mLogger.logNoPulsingNoAmbientEffect(sbn);
+            mLogger.logNoPulsingNoAmbientEffect(entry);
             return false;
         }
 
         if (entry.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) {
-            mLogger.logNoPulsingNotImportant(sbn);
+            mLogger.logNoPulsingNotImportant(entry);
             return false;
         }
-        mLogger.logPulsing(sbn);
+        mLogger.logPulsing(entry);
         return true;
     }
 
@@ -296,22 +294,20 @@
      * @return true if these checks pass, false if the notification should not alert
      */
     private boolean canAlertCommon(NotificationEntry entry) {
-        StatusBarNotification sbn = entry.getSbn();
-
         if (!mFlags.isNewPipelineEnabled() && mNotificationFilter.shouldFilterOut(entry)) {
-            mLogger.logNoAlertingFilteredOut(sbn);
+            mLogger.logNoAlertingFilteredOut(entry);
             return false;
         }
 
         for (int i = 0; i < mSuppressors.size(); i++) {
             if (mSuppressors.get(i).suppressInterruptions(entry)) {
-                mLogger.logNoAlertingSuppressedBy(sbn, mSuppressors.get(i), /* awake */ false);
+                mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ false);
                 return false;
             }
         }
 
         if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) {
-            mLogger.keyguardHideNotification(entry.getKey());
+            mLogger.keyguardHideNotification(entry);
             return false;
         }
 
@@ -329,12 +325,12 @@
 
         // Don't alert notifications that are suppressed due to group alert behavior
         if (sbn.isGroup() && sbn.getNotification().suppressAlertingDueToGrouping()) {
-            mLogger.logNoAlertingGroupAlertBehavior(sbn);
+            mLogger.logNoAlertingGroupAlertBehavior(entry);
             return false;
         }
 
         if (entry.hasJustLaunchedFullScreenIntent()) {
-            mLogger.logNoAlertingRecentFullscreen(sbn);
+            mLogger.logNoAlertingRecentFullscreen(entry);
             return false;
         }
 
@@ -352,7 +348,7 @@
 
         for (int i = 0; i < mSuppressors.size(); i++) {
             if (mSuppressors.get(i).suppressAwakeInterruptions(entry)) {
-                mLogger.logNoAlertingSuppressedBy(sbn, mSuppressors.get(i), /* awake */ true);
+                mLogger.logNoAlertingSuppressedBy(entry, mSuppressors.get(i), /* awake */ true);
                 return false;
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index 599039d..a493a67 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
 import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENABLE_REMOTE_INPUT;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
 
 import android.util.Log;
 import android.view.View;
@@ -247,7 +248,7 @@
     @Override
     @NonNull
     public String getNodeLabel() {
-        return mView.getEntry().getKey();
+        return logKey(mView.getEntry());
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index c661408..99a24cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -67,6 +67,7 @@
         mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon);
         mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile);
         mConversationSenderName = requireViewById(R.id.conversation_notification_sender);
+        applyTextColor(mConversationSenderName, mSecondaryTextColor);
         mFacePileSize = getResources()
                 .getDimensionPixelSize(R.dimen.conversation_single_line_face_pile_size);
         mFacePileAvatarSize = getResources()
@@ -75,6 +76,9 @@
                 .getDimensionPixelSize(R.dimen.conversation_single_line_avatar_size);
         mFacePileProtectionWidth = getResources().getDimensionPixelSize(
                 R.dimen.conversation_single_line_face_pile_protection_width);
+        mTransformationHelper.setCustomTransformation(
+                new FadeOutAndDownWithTitleTransformation(mConversationSenderName),
+                mConversationSenderName.getId());
         mTransformationHelper.addViewTransformingToSimilar(mConversationIconView);
         mTransformationHelper.addTransformedView(mConversationSenderName);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
index 40a44ff..77fd051 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
@@ -24,7 +24,6 @@
 import android.content.res.Resources;
 import android.service.notification.StatusBarNotification;
 import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -57,10 +56,8 @@
         mOverflowNumberPadding = res.getDimensionPixelSize(R.dimen.group_overflow_number_padding);
     }
 
-    private HybridNotificationView inflateHybridViewWithStyle(int style,
-            View contentView, ViewGroup parent) {
-        LayoutInflater inflater = new ContextThemeWrapper(mContext, style)
-                .getSystemService(LayoutInflater.class);
+    private HybridNotificationView inflateHybridView(View contentView, ViewGroup parent) {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
         int layout = contentView instanceof ConversationLayout
                 ? R.layout.hybrid_conversation_notification
                 : R.layout.hybrid_notification;
@@ -93,16 +90,8 @@
     public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
             View contentView, StatusBarNotification notification,
             ViewGroup parent) {
-        return bindFromNotificationWithStyle(reusableView, contentView, notification,
-                R.style.HybridNotification, parent);
-    }
-
-    private HybridNotificationView bindFromNotificationWithStyle(
-            HybridNotificationView reusableView, View contentView,
-            StatusBarNotification notification,
-            int style, ViewGroup parent) {
         if (reusableView == null) {
-            reusableView = inflateHybridViewWithStyle(style, contentView, parent);
+            reusableView = inflateHybridView(contentView, parent);
         }
         CharSequence titleText = resolveTitle(notification.getNotification());
         CharSequence contentText = resolveText(notification.getNotification());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
index c0d85a6..fc9d9e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
@@ -16,13 +16,18 @@
 
 package com.android.systemui.statusbar.notification.row;
 
+import static android.app.Notification.COLOR_INVALID;
+
 import android.annotation.Nullable;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.TextView;
 
+import androidx.annotation.ColorInt;
+
 import com.android.keyguard.AlphaOptimizedLinearLayout;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.CrossFadeHelper;
@@ -40,6 +45,8 @@
     protected final ViewTransformationHelper mTransformationHelper = new ViewTransformationHelper();
     protected TextView mTitleView;
     protected TextView mTextView;
+    protected int mPrimaryTextColor = COLOR_INVALID;
+    protected int mSecondaryTextColor = COLOR_INVALID;
 
     public HybridNotificationView(Context context) {
         this(context, null);
@@ -69,42 +76,37 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        resolveThemeTextColors();
         mTitleView = findViewById(R.id.notification_title);
         mTextView = findViewById(R.id.notification_text);
+        applyTextColor(mTitleView, mPrimaryTextColor);
+        applyTextColor(mTextView, mSecondaryTextColor);
         mTransformationHelper.setCustomTransformation(
-                new ViewTransformationHelper.CustomTransformation() {
-                    @Override
-                    public boolean transformTo(TransformState ownState, TransformableView notification,
-                            float transformationAmount) {
-                        // We want to transform to the same y location as the title
-                        TransformState otherState = notification.getCurrentState(
-                                TRANSFORMING_VIEW_TITLE);
-                        CrossFadeHelper.fadeOut(mTextView, transformationAmount);
-                        if (otherState != null) {
-                            ownState.transformViewVerticalTo(otherState, transformationAmount);
-                            otherState.recycle();
-                        }
-                        return true;
-                    }
-
-                    @Override
-                    public boolean transformFrom(TransformState ownState,
-                            TransformableView notification, float transformationAmount) {
-                        // We want to transform from the same y location as the title
-                        TransformState otherState = notification.getCurrentState(
-                                TRANSFORMING_VIEW_TITLE);
-                        CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */);
-                        if (otherState != null) {
-                            ownState.transformViewVerticalFrom(otherState, transformationAmount);
-                            otherState.recycle();
-                        }
-                        return true;
-                    }
-                }, TRANSFORMING_VIEW_TEXT);
+                new FadeOutAndDownWithTitleTransformation(mTextView),
+                TRANSFORMING_VIEW_TEXT);
         mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView);
         mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView);
     }
 
+    protected void applyTextColor(TextView textView, @ColorInt int textColor) {
+        if (textColor != COLOR_INVALID) {
+            textView.setTextColor(textColor);
+        }
+    }
+
+    private void resolveThemeTextColors() {
+        try (TypedArray ta = mContext.getTheme().obtainStyledAttributes(
+                android.R.style.Theme_DeviceDefault_DayNight, new int[]{
+                        android.R.attr.textColorPrimary,
+                        android.R.attr.textColorSecondary
+                })) {
+            if (ta != null) {
+                mPrimaryTextColor = ta.getColor(0, mPrimaryTextColor);
+                mSecondaryTextColor = ta.getColor(1, mSecondaryTextColor);
+            }
+        }
+    }
+
     public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
             @Nullable View contentView) {
         mTitleView.setText(title);
@@ -152,4 +154,40 @@
 
     @Override
     public void setNotificationFaded(boolean faded) {}
+
+    protected static class FadeOutAndDownWithTitleTransformation extends
+            ViewTransformationHelper.CustomTransformation {
+
+        private final View mView;
+
+        public FadeOutAndDownWithTitleTransformation(View view) {
+            mView = view;
+        }
+
+        @Override
+        public boolean transformTo(TransformState ownState, TransformableView notification,
+                float transformationAmount) {
+            // We want to transform to the same y location as the title
+            TransformState otherState = notification.getCurrentState(TRANSFORMING_VIEW_TITLE);
+            CrossFadeHelper.fadeOut(mView, transformationAmount);
+            if (otherState != null) {
+                ownState.transformViewVerticalTo(otherState, transformationAmount);
+                otherState.recycle();
+            }
+            return true;
+        }
+
+        @Override
+        public boolean transformFrom(TransformState ownState,
+                TransformableView notification, float transformationAmount) {
+            // We want to transform from the same y location as the title
+            TransformState otherState = notification.getCurrentState(TRANSFORMING_VIEW_TITLE);
+            CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */);
+            if (otherState != null) {
+                ownState.transformViewVerticalFrom(otherState, transformationAmount);
+                otherState.recycle();
+            }
+            return true;
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java
index f693ebb..ea564dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipeline.java
@@ -112,7 +112,8 @@
     public void manageRow(
             @NonNull NotificationEntry entry,
             @NonNull ExpandableNotificationRow row) {
-        mLogger.logManagedRow(entry.getKey());
+        mLogger.logManagedRow(entry);
+        mLogger.logManagedRow(entry);
 
         final BindEntry bindEntry = getBindEntry(entry);
         if (bindEntry == null) {
@@ -154,12 +155,12 @@
      * the real work once rather than repeatedly start and cancel it.
      */
     private void requestPipelineRun(NotificationEntry entry) {
-        mLogger.logRequestPipelineRun(entry.getKey());
+        mLogger.logRequestPipelineRun(entry);
 
         final BindEntry bindEntry = getBindEntry(entry);
         if (bindEntry.row == null) {
             // Row is not managed yet but may be soon. Stop for now.
-            mLogger.logRequestPipelineRowNotSet(entry.getKey());
+            mLogger.logRequestPipelineRowNotSet(entry);
             return;
         }
 
@@ -177,7 +178,7 @@
      * callbacks when the run finishes. If a run is already in progress, it is restarted.
      */
     private void startPipeline(NotificationEntry entry) {
-        mLogger.logStartPipeline(entry.getKey());
+        mLogger.logStartPipeline(entry);
 
         if (mStage == null) {
             throw new IllegalStateException("No stage was ever set on the pipeline");
@@ -193,7 +194,7 @@
         final BindEntry bindEntry = getBindEntry(entry);
         final Set<BindCallback> callbacks = bindEntry.callbacks;
 
-        mLogger.logFinishedPipeline(entry.getKey(), callbacks.size());
+        mLogger.logFinishedPipeline(entry, callbacks.size());
 
         bindEntry.invalidated = false;
         // Move all callbacks to separate list as callbacks may themselves add/remove callbacks.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
index ec406f0..ab91926 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifBindPipelineLogger.kt
@@ -19,6 +19,8 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class NotifBindPipelineLogger @Inject constructor(
@@ -32,41 +34,41 @@
         })
     }
 
-    fun logManagedRow(notifKey: String) {
+    fun logManagedRow(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
+            str1 = entry.logKey
         }, {
             "Row set for notif: $str1"
         })
     }
 
-    fun logRequestPipelineRun(notifKey: String) {
+    fun logRequestPipelineRun(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
+            str1 = entry.logKey
         }, {
             "Request pipeline run for notif: $str1"
         })
     }
 
-    fun logRequestPipelineRowNotSet(notifKey: String) {
+    fun logRequestPipelineRowNotSet(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
+            str1 = entry.logKey
         }, {
             "Row is not set so pipeline will not run. notif = $str1"
         })
     }
 
-    fun logStartPipeline(notifKey: String) {
+    fun logStartPipeline(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
+            str1 = entry.logKey
         }, {
             "Start pipeline for notif: $str1"
         })
     }
 
-    fun logFinishedPipeline(notifKey: String, numCallbacks: Int) {
+    fun logFinishedPipeline(entry: NotificationEntry, numCallbacks: Int) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
+            str1 = entry.logKey
             int1 = numCallbacks
         }, {
             "Finished pipeline for notif $str1 with $int1 callbacks"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
index 134f24e..27aa4b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java
@@ -266,9 +266,14 @@
         snooze.setOnClickListener(mOnSnoozeClick);
         */
 
-        if (mAppBubble == BUBBLE_PREFERENCE_ALL) {
-            ((TextView) findViewById(R.id.default_summary)).setText(getResources().getString(
+        TextView defaultSummaryTextView = findViewById(R.id.default_summary);
+        if (mAppBubble == BUBBLE_PREFERENCE_ALL
+                && BubblesManager.areBubblesEnabled(mContext, mSbn.getUser())) {
+            defaultSummaryTextView.setText(getResources().getString(
                     R.string.notification_channel_summary_default_with_bubbles, mAppName));
+        } else {
+            defaultSummaryTextView.setText(getResources().getString(
+                    R.string.notification_channel_summary_default));
         }
 
         findViewById(R.id.priority).setOnClickListener(mOnFavoriteClick);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index 3616f8f..81cf146 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -57,7 +57,7 @@
             @NonNull StageCallback callback) {
         RowContentBindParams params = getStageParams(entry);
 
-        mLogger.logStageParams(entry.getKey(), params.toString());
+        mLogger.logStageParams(entry, params);
 
         // Resolve content to bind/unbind.
         @InflationFlag int inflationFlags = params.getContentViews();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
index 29cce33..f9923b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStageLogger.kt
@@ -19,17 +19,19 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class RowContentBindStageLogger @Inject constructor(
     @NotificationLog private val buffer: LogBuffer
 ) {
-    fun logStageParams(notifKey: String, stageParams: String) {
+    fun logStageParams(entry: NotificationEntry, stageParams: RowContentBindParams) {
         buffer.log(TAG, INFO, {
-            str1 = notifKey
-            str2 = stageParams
+            str1 = entry.logKey
+            str2 = stageParams.toString()
         }, {
-            "Invalidated notif $str1 with params: \n$str2"
+            "Invalidated notif $str1 with params: $str2"
         })
     }
 }
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 213f00b..2fd02d9 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
@@ -748,19 +748,20 @@
         }
     }
 
-    private void logHunSkippedForUnexpectedState(String key, boolean expected, boolean actual) {
+    private void logHunSkippedForUnexpectedState(ExpandableNotificationRow enr,
+            boolean expected, boolean actual) {
         if (mLogger == null) return;
-        mLogger.hunSkippedForUnexpectedState(key, expected, actual);
+        mLogger.hunSkippedForUnexpectedState(enr.getEntry(), expected, actual);
     }
 
-    private void logHunAnimationSkipped(String key, String reason) {
+    private void logHunAnimationSkipped(ExpandableNotificationRow enr, String reason) {
         if (mLogger == null) return;
-        mLogger.hunAnimationSkipped(key, reason);
+        mLogger.hunAnimationSkipped(enr.getEntry(), reason);
     }
 
-    private void logHunAnimationEventAdded(String key, int type) {
+    private void logHunAnimationEventAdded(ExpandableNotificationRow enr, int type) {
         if (mLogger == null) return;
-        mLogger.hunAnimationEventAdded(key, type);
+        mLogger.hunAnimationEventAdded(enr.getEntry(), type);
     }
 
     private void onDrawDebug(Canvas canvas) {
@@ -3174,7 +3175,7 @@
             if (isHeadsUp != row.isHeadsUp()) {
                 // For cases where we have a heads up showing and appearing again we shouldn't
                 // do the animations at all.
-                logHunSkippedForUnexpectedState(key, isHeadsUp, row.isHeadsUp());
+                logHunSkippedForUnexpectedState(row, isHeadsUp, row.isHeadsUp());
                 continue;
             }
             int type = AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER;
@@ -3192,7 +3193,7 @@
                 if (row.isChildInGroup()) {
                     // We can otherwise get stuck in there if it was just isolated
                     row.setHeadsUpAnimatingAway(false);
-                    logHunAnimationSkipped(key, "row is child in group");
+                    logHunAnimationSkipped(row, "row is child in group");
                     continue;
                 }
             } else {
@@ -3200,7 +3201,7 @@
                 if (viewState == null) {
                     // A view state was never generated for this view, so we don't need to animate
                     // this. This may happen with notification children.
-                    logHunAnimationSkipped(key, "row has no viewState");
+                    logHunAnimationSkipped(row, "row has no viewState");
                     continue;
                 }
                 if (isHeadsUp && (mAddedHeadsUpChildren.contains(row) || pinnedAndClosed)) {
@@ -3224,7 +3225,7 @@
                         + " onBottom=" + onBottom
                         + " row=" + row.getEntry().getKey());
             }
-            logHunAnimationEventAdded(key, type);
+            logHunAnimationEventAdded(row, type);
         }
         mHeadsUpChangeAnimations.clear();
         mAddedHeadsUpChildren.clear();
@@ -4360,8 +4361,6 @@
 
     /**
      * Update colors of "dismiss" and "empty shade" views.
-     *
-     * @param lightTheme True if light theme should be used.
      */
     @ShadeViewRefactor(RefactorComponent.DECORATOR)
     void updateDecorViews() {
@@ -4777,8 +4776,7 @@
                 if (SPEW) {
                     Log.v(TAG, "generateHeadsUpAnimation: previous hun appear animation cancelled");
                 }
-                logHunAnimationSkipped(row.getEntry().getKey(),
-                        "previous hun appear animation cancelled");
+                logHunAnimationSkipped(row, "previous hun appear animation cancelled");
                 return;
             }
             mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 04bf621..5f79c0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -3,21 +3,27 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.*
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER
 import javax.inject.Inject
 
 class NotificationStackScrollLogger @Inject constructor(
     @NotificationHeadsUpLog private val buffer: LogBuffer
 ) {
-    fun hunAnimationSkipped(key: String, reason: String) {
+    fun hunAnimationSkipped(entry: NotificationEntry, reason: String) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = reason
         }, {
             "heads up animation skipped: key: $str1 reason: $str2"
         })
     }
-    fun hunAnimationEventAdded(key: String, type: Int) {
+    fun hunAnimationEventAdded(entry: NotificationEntry, type: Int) {
         val reason: String
         reason = if (type == ANIMATION_TYPE_HEADS_UP_DISAPPEAR) {
             "HEADS_UP_DISAPPEAR"
@@ -33,16 +39,16 @@
             type.toString()
         }
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             str2 = reason
         }, {
             "heads up animation added: $str1 with type $str2"
         })
     }
 
-    fun hunSkippedForUnexpectedState(key: String, expected: Boolean, actual: Boolean) {
+    fun hunSkippedForUnexpectedState(entry: NotificationEntry, expected: Boolean, actual: Boolean) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             bool1 = expected
             bool2 = actual
         }, {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
index 77377af..cb4a088 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateLogger.kt
@@ -3,6 +3,7 @@
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.LogLevel
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 class StackStateLogger @Inject constructor(
@@ -10,7 +11,7 @@
 ) {
     fun logHUNViewDisappearing(key: String) {
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up view disappearing $str1 "
         })
@@ -18,7 +19,7 @@
 
     fun logHUNViewAppearing(key: String) {
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up notification view appearing $str1 "
         })
@@ -26,7 +27,7 @@
 
     fun logHUNViewDisappearingWithRemoveEvent(key: String) {
         buffer.log(TAG, LogLevel.ERROR, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up view disappearing $str1 for ANIMATION_TYPE_REMOVE"
         })
@@ -34,7 +35,7 @@
 
     fun logHUNViewAppearingWithAddEvent(key: String) {
         buffer.log(TAG, LogLevel.ERROR, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up view disappearing $str1 for ANIMATION_TYPE_ADD"
         })
@@ -42,7 +43,7 @@
 
     fun disappearAnimationEnded(key: String) {
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up notification disappear animation ended $str1 "
         })
@@ -50,7 +51,7 @@
 
     fun appearAnimationEnded(key: String) {
         buffer.log(TAG, LogLevel.INFO, {
-            str1 = key
+            str1 = logKey(key)
         }, {
             "Heads up notification appear animation ended $str1 "
         })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index fd307df..93b2e41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -28,17 +28,13 @@
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE;
 
-import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
@@ -46,13 +42,8 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.MediaStore;
-import android.service.media.CameraPrewarmService;
 import android.service.quickaccesswallet.GetWalletCardsError;
 import android.service.quickaccesswallet.GetWalletCardsResponse;
 import android.service.quickaccesswallet.QuickAccessWalletClient;
@@ -172,20 +163,6 @@
     private KeyguardAffordanceHelper mAffordanceHelper;
     private FalsingManager mFalsingManager;
     private boolean mUserSetupComplete;
-    private boolean mPrewarmBound;
-    private Messenger mPrewarmMessenger;
-    private final ServiceConnection mPrewarmConnection = new ServiceConnection() {
-
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            mPrewarmMessenger = new Messenger(service);
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            mPrewarmMessenger = null;
-        }
-    };
 
     private boolean mLeftIsVoiceAssist;
     private Drawable mLeftAssistIcon;
@@ -602,46 +579,6 @@
         }
     }
 
-    public void bindCameraPrewarmService() {
-        Intent intent = getCameraIntent();
-        ActivityInfo targetInfo = mActivityIntentHelper.getTargetActivityInfo(intent,
-                KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */);
-        if (targetInfo != null && targetInfo.metaData != null) {
-            String clazz = targetInfo.metaData.getString(
-                    MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE);
-            if (clazz != null) {
-                Intent serviceIntent = new Intent();
-                serviceIntent.setClassName(targetInfo.packageName, clazz);
-                serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM);
-                try {
-                    if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection,
-                            Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
-                            new UserHandle(UserHandle.USER_CURRENT))) {
-                        mPrewarmBound = true;
-                    }
-                } catch (SecurityException e) {
-                    Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName
-                            + " class=" + clazz, e);
-                }
-            }
-        }
-    }
-
-    public void unbindCameraPrewarmService(boolean launched) {
-        if (mPrewarmBound) {
-            if (mPrewarmMessenger != null && launched) {
-                try {
-                    mPrewarmMessenger.send(Message.obtain(null /* handler */,
-                            CameraPrewarmService.MSG_CAMERA_FIRED));
-                } catch (RemoteException e) {
-                    Log.w(TAG, "Error sending camera fired message", e);
-                }
-            }
-            mContext.unbindService(mPrewarmConnection);
-            mPrewarmBound = false;
-        }
-    }
-
     public void launchCamera(String source) {
         final Intent intent = getCameraIntent();
         intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source);
@@ -651,8 +588,6 @@
             AsyncTask.execute(new Runnable() {
                 @Override
                 public void run() {
-                    int result = ActivityManager.START_CANCELED;
-
                     // Normally an activity will set it's requested rotation
                     // animation on its window. However when launching an activity
                     // causes the orientation to change this is too late. In these cases
@@ -666,7 +601,7 @@
                     o.setRotationAnimationHint(
                             WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
                     try {
-                        result = ActivityTaskManager.getService().startActivityAsUser(
+                        ActivityTaskManager.getService().startActivityAsUser(
                                 null, getContext().getBasePackageName(),
                                 getContext().getAttributionTag(), intent,
                                 intent.resolveTypeIfNeeded(getContext().getContentResolver()),
@@ -675,25 +610,12 @@
                     } catch (RemoteException e) {
                         Log.w(TAG, "Unable to start camera activity", e);
                     }
-                    final boolean launched = isSuccessfulLaunch(result);
-                    post(new Runnable() {
-                        @Override
-                        public void run() {
-                            unbindCameraPrewarmService(launched);
-                        }
-                    });
                 }
             });
         } else {
             // We need to delay starting the activity because ResolverActivity finishes itself if
             // launched behind lockscreen.
-            mActivityStarter.startActivity(intent, false /* dismissShade */,
-                    new ActivityStarter.Callback() {
-                        @Override
-                        public void onActivityStarted(int resultCode) {
-                            unbindCameraPrewarmService(isSuccessfulLaunch(resultCode));
-                        }
-                    });
+            mActivityStarter.startActivity(intent, false /* dismissShade */);
         }
     }
 
@@ -705,12 +627,6 @@
         dozeTimeTick();
     }
 
-    private static boolean isSuccessfulLaunch(int result) {
-        return result == ActivityManager.START_SUCCESS
-                || result == ActivityManager.START_DELIVERED_TO_TOP
-                || result == ActivityManager.START_TASK_TO_FRONT;
-    }
-
     public void launchLeftAffordance() {
         if (mLeftIsVoiceAssist) {
             launchVoiceAssist();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 6009eba..fbbb587 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -1670,7 +1670,6 @@
         setQsExpansionEnabled();
     }
 
-    @Override
     public void resetViews(boolean animate) {
         mIsLaunchTransitionFinished = false;
         mBlockTouches = false;
@@ -4592,13 +4591,6 @@
         @Override
         public void onSwipingStarted(boolean rightIcon) {
             mFalsingCollector.onAffordanceSwipingStarted(rightIcon);
-            boolean
-                    camera =
-                    mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon
-                            : rightIcon;
-            if (camera) {
-                mKeyguardBottomArea.bindCameraPrewarmService();
-            }
             mView.requestDisallowInterceptTouchEvent(true);
             mOnlyAffordanceInThisMotion = true;
             mQsTracking = false;
@@ -4607,7 +4599,6 @@
         @Override
         public void onSwipingAborted() {
             mFalsingCollector.onAffordanceSwipingAborted();
-            mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
index 18f0fb3..f5828f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/OWNERS
@@ -1,3 +1,16 @@
 per-file *Notification* = set noparent
 per-file *Notification* = file:../notification/OWNERS
-per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com
\ No newline at end of file
+
+per-file NotificationIcon* = ccassidy@google.com, evanlaird@google.com, pixel@google.com
+
+per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
+per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
+
+per-file NotificationShadeWindowControllerImpl.java = dupin@google.com, cinek@google.com, beverlyt@google.com, pixel@google.com, juliacr@google.com
+per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
+per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
+
+per-file NotificationPanelUnfoldAnimationController.kt = alexflo@google.com, jeffdq@google.com, juliacr@google.com
+
+per-file NotificationPanelView.java = pixel@google.com, cinek@google.com, juliacr@google.com
+per-file NotificationPanelViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
index 9f0ecb9..ed12b00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -1163,8 +1163,6 @@
                 mTouchDisabled ? "T" : "f"));
     }
 
-    public abstract void resetViews(boolean animate);
-
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         mHeadsUpManager = headsUpManager;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 36a0456..26bc3e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -93,6 +93,17 @@
     }
 
     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) {
+        // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
+        // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
+        // the content and attach listeners.
+        this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class),
+                Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class),
+                Dependency.get(DialogLaunchAnimator.class));
+    }
+
+    public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
+            SystemUIDialogManager dialogManager, SysUiState sysUiState,
+            BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) {
         super(context, theme);
         mContext = context;
 
@@ -101,13 +112,10 @@
         attrs.setTitle(getClass().getSimpleName());
         getWindow().setAttributes(attrs);
 
-        mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null;
-
-        // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
-        // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
-        // the content and attach listeners.
-        mDialogManager = Dependency.get(SystemUIDialogManager.class);
-        mSysUiState = Dependency.get(SysUiState.class);
+        mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher,
+                dialogLaunchAnimator) : null;
+        mDialogManager = dialogManager;
+        mSysUiState = sysUiState;
     }
 
     @Override
@@ -326,7 +334,10 @@
      * @param dismissAction An action to run when the dialog is dismissed.
      */
     public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) {
-        DismissReceiver dismissReceiver = new DismissReceiver(dialog);
+        // TODO(b/219008720): Remove those calls to Dependency.get.
+        DismissReceiver dismissReceiver = new DismissReceiver(dialog,
+                Dependency.get(BroadcastDispatcher.class),
+                Dependency.get(DialogLaunchAnimator.class));
         dialog.setOnDismissListener(d -> {
             dismissReceiver.unregister();
             if (dismissAction != null) dismissAction.run();
@@ -408,11 +419,11 @@
         private final BroadcastDispatcher mBroadcastDispatcher;
         private final DialogLaunchAnimator mDialogLaunchAnimator;
 
-        DismissReceiver(Dialog dialog) {
+        DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher,
+                DialogLaunchAnimator dialogLaunchAnimator) {
             mDialog = dialog;
-            // TODO(b/219008720): Remove those calls to Dependency.get.
-            mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
-            mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class);
+            mBroadcastDispatcher = broadcastDispatcher;
+            mDialogLaunchAnimator = dialogLaunchAnimator;
         }
 
         void register() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt
index 1740bcb..16f28e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionController.kt
@@ -2,10 +2,13 @@
 
 import android.content.res.Configuration
 import android.content.res.Resources
+import android.util.MathUtils.constrain
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -21,7 +24,8 @@
     configurationController: ConfigurationController,
     dumpManager: DumpManager,
     private val scrimController: ScrimController,
-    @Main private val resources: Resources
+    @Main private val resources: Resources,
+    private val statusBarStateController: SysuiStatusBarStateController,
 ) {
 
     private var inSplitShade = false
@@ -55,19 +59,23 @@
     }
 
     private fun calculateScrimExpansionFraction(expansionEvent: PanelExpansionChangeEvent): Float {
-        return if (inSplitShade) {
-            expansionEvent.dragDownPxAmount / splitShadeScrimTransitionDistance
+        return if (inSplitShade && isScreenUnlocked()) {
+            constrain(expansionEvent.dragDownPxAmount / splitShadeScrimTransitionDistance, 0f, 1f)
         } else {
             expansionEvent.fraction
         }
     }
 
+    private fun isScreenUnlocked() =
+        statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE
+
     private fun dump(printWriter: PrintWriter, args: Array<String>) {
         printWriter.println(
             """
                 ScrimShadeTransitionController:
                   Resources:
                     inSplitShade: $inSplitShade
+                    isScreenUnlocked: ${isScreenUnlocked()}
                     splitShadeScrimTransitionDistance: $splitShadeScrimTransitionDistance
                   State:
                     lastExpansionFraction: $lastExpansionFraction
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index a89c128..753e940 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -119,6 +119,17 @@
     }
 
     /**
+     * Returns {@code true} if the charging source is
+     * {@link android.os.BatteryManager#BATTERY_PLUGGED_DOCK}.
+     *
+     * <P>Note that charging from dock is not considered as wireless charging. In other words,
+     * {@link BatteryController#isWirelessCharging()} and this are mutually exclusive.
+     */
+    default boolean isChargingSourceDock() {
+        return false;
+    }
+
+    /**
      * A listener that will be notified whenever a change in battery level or power save mode has
      * occurred.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 917a5e0..33ddf7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -76,7 +76,7 @@
 
     protected int mLevel;
     protected boolean mPluggedIn;
-    private boolean mPluggedInWireless;
+    private int mPluggedChargingSource;
     protected boolean mCharging;
     private boolean mStateUnknown = false;
     private boolean mCharged;
@@ -195,10 +195,8 @@
             mLevel = (int)(100f
                     * intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
                     / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100));
-            mPluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
-            mPluggedInWireless = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0)
-                    == BatteryManager.BATTERY_PLUGGED_WIRELESS;
-
+            mPluggedChargingSource = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
+            mPluggedIn = mPluggedChargingSource != 0;
             final int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,
                     BatteryManager.BATTERY_STATUS_UNKNOWN);
             mCharged = status == BatteryManager.BATTERY_STATUS_FULL;
@@ -284,7 +282,7 @@
 
     @Override
     public boolean isPluggedInWireless() {
-        return mPluggedInWireless;
+        return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS;
     }
 
     @Override
@@ -441,4 +439,9 @@
         registerReceiver();
         updatePowerSave();
     }
+
+    @Override
+    public boolean isChargingSourceDock() {
+        return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_DOCK;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
index 4cf1d2b..9946b4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -17,15 +17,11 @@
 package com.android.systemui.statusbar.policy;
 
 import android.annotation.WorkerThread;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.camera2.CameraAccessException;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraManager;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
 import android.provider.Settings;
 import android.provider.Settings.Secure;
 import android.text.TextUtils;
@@ -33,12 +29,19 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.internal.annotations.GuardedBy;
+import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.inject.Inject;
 
@@ -59,65 +62,88 @@
         "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED";
 
     private final CameraManager mCameraManager;
-    private final Context mContext;
-    /** Call {@link #ensureHandler()} before using */
-    private Handler mHandler;
+    private final Executor mExecutor;
+    private final SecureSettings mSecureSettings;
+    private final DumpManager mDumpManager;
+    private final BroadcastSender mBroadcastSender;
 
-    /** Lock on mListeners when accessing */
+    private final boolean mHasFlashlight;
+
+    @GuardedBy("mListeners")
     private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1);
 
-    /** Lock on {@code this} when accessing */
+    @GuardedBy("this")
     private boolean mFlashlightEnabled;
-
-    private String mCameraId;
+    @GuardedBy("this")
     private boolean mTorchAvailable;
 
-    @Inject
-    public FlashlightControllerImpl(Context context, DumpManager dumpManager) {
-        mContext = context;
-        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+    private final AtomicReference<String> mCameraId;
+    private final AtomicBoolean mInitted = new AtomicBoolean(false);
 
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-        tryInitCamera();
+    @Inject
+    public FlashlightControllerImpl(
+            DumpManager dumpManager,
+            CameraManager cameraManager,
+            @Background Executor bgExecutor,
+            SecureSettings secureSettings,
+            BroadcastSender broadcastSender,
+            PackageManager packageManager
+    ) {
+        mCameraManager = cameraManager;
+        mExecutor = bgExecutor;
+        mCameraId = new AtomicReference<>(null);
+        mSecureSettings = secureSettings;
+        mDumpManager = dumpManager;
+        mBroadcastSender = broadcastSender;
+
+        mHasFlashlight = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+        init();
     }
 
+    private void init() {
+        if (!mInitted.getAndSet(true)) {
+            mDumpManager.registerDumpable(getClass().getSimpleName(), this);
+            mExecutor.execute(this::tryInitCamera);
+        }
+    }
+
+    @WorkerThread
     private void tryInitCamera() {
+        if (!mHasFlashlight || mCameraId.get() != null) return;
         try {
-            mCameraId = getCameraId();
+            mCameraId.set(getCameraId());
         } catch (Throwable e) {
             Log.e(TAG, "Couldn't initialize.", e);
             return;
         }
 
-        if (mCameraId != null) {
-            ensureHandler();
-            mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
+        if (mCameraId.get() != null) {
+            mCameraManager.registerTorchCallback(mExecutor, mTorchCallback);
         }
     }
 
     public void setFlashlight(boolean enabled) {
-        boolean pendingError = false;
-        synchronized (this) {
-            if (mCameraId == null) return;
-            if (mFlashlightEnabled != enabled) {
-                mFlashlightEnabled = enabled;
-                try {
-                    mCameraManager.setTorchMode(mCameraId, enabled);
-                } catch (CameraAccessException e) {
-                    Log.e(TAG, "Couldn't set torch mode", e);
-                    mFlashlightEnabled = false;
-                    pendingError = true;
+        if (!mHasFlashlight) return;
+        if (mCameraId.get() == null) {
+            mExecutor.execute(this::tryInitCamera);
+        }
+        mExecutor.execute(() -> {
+            if (mCameraId.get() == null) return;
+            synchronized (this) {
+                if (mFlashlightEnabled != enabled) {
+                    try {
+                        mCameraManager.setTorchMode(mCameraId.get(), enabled);
+                    } catch (CameraAccessException e) {
+                        Log.e(TAG, "Couldn't set torch mode", e);
+                        dispatchError();
+                    }
                 }
             }
-        }
-        dispatchModeChanged(mFlashlightEnabled);
-        if (pendingError) {
-            dispatchError();
-        }
+        });
     }
 
     public boolean hasFlashlight() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+        return mHasFlashlight;
     }
 
     public synchronized boolean isEnabled() {
@@ -131,13 +157,13 @@
     @Override
     public void addCallback(@NonNull FlashlightListener l) {
         synchronized (mListeners) {
-            if (mCameraId == null) {
-                tryInitCamera();
+            if (mCameraId.get() == null) {
+                mExecutor.execute(this::tryInitCamera);
             }
             cleanUpListenersLocked(l);
             mListeners.add(new WeakReference<>(l));
-            l.onFlashlightAvailabilityChanged(mTorchAvailable);
-            l.onFlashlightChanged(mFlashlightEnabled);
+            l.onFlashlightAvailabilityChanged(isAvailable());
+            l.onFlashlightChanged(isEnabled());
         }
     }
 
@@ -148,14 +174,7 @@
         }
     }
 
-    private synchronized void ensureHandler() {
-        if (mHandler == null) {
-            HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
-            thread.start();
-            mHandler = new Handler(thread.getLooper());
-        }
-    }
-
+    @WorkerThread
     private String getCameraId() throws CameraAccessException {
         String[] ids = mCameraManager.getCameraIdList();
         for (String id : ids) {
@@ -221,10 +240,9 @@
         @Override
         @WorkerThread
         public void onTorchModeUnavailable(String cameraId) {
-            if (TextUtils.equals(cameraId, mCameraId)) {
+            if (TextUtils.equals(cameraId, mCameraId.get())) {
                 setCameraAvailable(false);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 0);
+                mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 0);
 
             }
         }
@@ -232,14 +250,12 @@
         @Override
         @WorkerThread
         public void onTorchModeChanged(String cameraId, boolean enabled) {
-            if (TextUtils.equals(cameraId, mCameraId)) {
+            if (TextUtils.equals(cameraId, mCameraId.get())) {
                 setCameraAvailable(true);
                 setTorchMode(enabled);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
-                Settings.Secure.putInt(
-                    mContext.getContentResolver(), Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
-                mContext.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
+                mSecureSettings.putInt(Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
+                mSecureSettings.putInt(Secure.FLASHLIGHT_ENABLED, enabled ? 1 : 0);
+                mBroadcastSender.sendBroadcast(new Intent(ACTION_FLASHLIGHT_CHANGED));
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
index bce5a15..d3837d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -142,7 +142,7 @@
 
     protected void setEntryPinned(
             @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
-        mLogger.logSetEntryPinned(headsUpEntry.mEntry.getKey(), isPinned);
+        mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned);
         NotificationEntry entry = headsUpEntry.mEntry;
         if (entry.isRowPinned() != isPinned) {
             entry.setRowPinned(isPinned);
@@ -183,7 +183,7 @@
         entry.setHeadsUp(false);
         setEntryPinned((HeadsUpEntry) alertEntry, false /* isPinned */);
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
-        mLogger.logNotificationActuallyRemoved(entry.getKey());
+        mLogger.logNotificationActuallyRemoved(entry);
         for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpStateChanged(entry, false);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 6a74ba9..d7c81af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -20,6 +20,8 @@
 import com.android.systemui.log.LogLevel.INFO
 import com.android.systemui.log.LogLevel.VERBOSE
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
 import javax.inject.Inject
 
 /** Logger for [HeadsUpManager]. */
@@ -56,9 +58,9 @@
         })
     }
 
-    fun logShowNotification(key: String) {
+    fun logShowNotification(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "show notification $str1"
         })
@@ -66,16 +68,16 @@
 
     fun logRemoveNotification(key: String, releaseImmediately: Boolean) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = logKey(key)
             bool1 = releaseImmediately
         }, {
             "remove notification $str1 releaseImmediately: $bool1"
         })
     }
 
-    fun logNotificationActuallyRemoved(key: String) {
+    fun logNotificationActuallyRemoved(entry: NotificationEntry) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
         }, {
             "notification removed $str1 "
         })
@@ -83,7 +85,7 @@
 
     fun logUpdateNotification(key: String, alert: Boolean, hasEntry: Boolean) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = logKey(key)
             bool1 = alert
             bool2 = hasEntry
         }, {
@@ -91,12 +93,12 @@
         })
     }
 
-    fun logUpdateEntry(key: String, updatePostTime: Boolean) {
+    fun logUpdateEntry(entry: NotificationEntry, updatePostTime: Boolean) {
         buffer.log(TAG, INFO, {
-            str1 = key
+            str1 = entry.logKey
             bool1 = updatePostTime
         }, {
-            "update entry $key updatePostTime: $bool1"
+            "update entry $str1 updatePostTime: $bool1"
         })
     }
 
@@ -108,9 +110,9 @@
         })
     }
 
-    fun logSetEntryPinned(key: String, isPinned: Boolean) {
+    fun logSetEntryPinned(entry: NotificationEntry, isPinned: Boolean) {
         buffer.log(TAG, VERBOSE, {
-            str1 = key
+            str1 = entry.logKey
             bool1 = isPinned
         }, {
             "set entry pinned $str1 pinned: $bool1"
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
index 4e9030f..dac8a0b 100644
--- a/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/Monitor.java
@@ -92,7 +92,15 @@
     }
 
     private void updateConditionMetState(Condition condition) {
-        mConditions.get(condition).stream().forEach(token -> mSubscriptions.get(token).update());
+        final ArraySet<Subscription.Token> subscriptions = mConditions.get(condition);
+
+        // It's possible the condition was removed between the time the callback occurred and
+        // update was executed on the main thread.
+        if (subscriptions == null) {
+            return;
+        }
+
+        subscriptions.stream().forEach(token -> mSubscriptions.get(token).update());
     }
 
     /**
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 7de4586..bc35142 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -27,7 +27,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 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.verify;
 import static org.mockito.Mockito.when;
@@ -68,8 +67,6 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper()
 public class KeyguardSecurityContainerControllerTest extends SysuiTestCase {
-    private static final int VIEW_WIDTH = 1600;
-
     @Rule
     public MockitoRule mRule = MockitoJUnit.rule();
 
@@ -141,7 +138,6 @@
         when(mResources.getConfiguration()).thenReturn(mConfiguration);
         when(mView.getContext()).thenReturn(mContext);
         when(mView.getResources()).thenReturn(mResources);
-        when(mView.getWidth()).thenReturn(VIEW_WIDTH);
         when(mAdminSecondaryLockScreenControllerFactory.create(any(KeyguardSecurityCallback.class)))
                 .thenReturn(mAdminSecondaryLockScreenController);
         when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController);
@@ -212,49 +208,26 @@
                 mUserSwitcherController);
     }
 
-    private void touchDownLeftSide() {
+    private void touchDown() {
         mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent(
                 MotionEvent.obtain(
                         /* downTime= */0,
                         /* eventTime= */0,
                         MotionEvent.ACTION_DOWN,
-                        /* x= */VIEW_WIDTH / 3f,
-                        /* y= */0,
-                        /* metaState= */0));
-    }
-
-    private void touchDownRightSide() {
-        mKeyguardSecurityContainerController.mGlobalTouchListener.onTouchEvent(
-                MotionEvent.obtain(
-                        /* downTime= */0,
-                        /* eventTime= */0,
-                        MotionEvent.ACTION_DOWN,
-                        /* x= */(VIEW_WIDTH / 3f) * 2,
+                        /* x= */0,
                         /* y= */0,
                         /* metaState= */0));
     }
 
     @Test
-    public void onInterceptTap_inhibitsFalsingInOneHandedMode() {
-        when(mView.getMode()).thenReturn(MODE_ONE_HANDED);
-        when(mView.isOneHandedModeLeftAligned()).thenReturn(true);
+    public void onInterceptTap_inhibitsFalsingInSidedSecurityMode() {
 
-        touchDownLeftSide();
+        when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(false);
+        touchDown();
         verify(mFalsingCollector, never()).avoidGesture();
 
-        // Now on the right.
-        touchDownRightSide();
-        verify(mFalsingCollector).avoidGesture();
-
-        // Move and re-test
-        reset(mFalsingCollector);
-        when(mView.isOneHandedModeLeftAligned()).thenReturn(false);
-
-        // On the right...
-        touchDownRightSide();
-        verify(mFalsingCollector, never()).avoidGesture();
-
-        touchDownLeftSide();
+        when(mView.isTouchOnTheOtherSideOfSecurity(any())).thenReturn(true);
+        touchDown();
         verify(mFalsingCollector).avoidGesture();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index d49f4d8..f2ac0c7 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -16,6 +16,11 @@
 
 package com.android.keyguard;
 
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE;
+import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
+import static android.provider.Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
@@ -25,7 +30,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -34,14 +41,13 @@
 import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 import android.graphics.Insets;
-import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.Gravity;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
-import android.view.WindowInsetsController;
 import android.widget.FrameLayout;
 
 import androidx.test.filters.SmallTest;
@@ -70,6 +76,9 @@
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper()
 public class KeyguardSecurityContainerTest extends SysuiTestCase {
+
+    private static final int VIEW_WIDTH = 1600;
+
     private int mScreenWidth;
     private int mFakeMeasureSpec;
 
@@ -77,8 +86,6 @@
     public MockitoRule mRule = MockitoJUnit.rule();
 
     @Mock
-    private WindowInsetsController mWindowInsetsController;
-    @Mock
     private KeyguardSecurityViewFlipper mSecurityViewFlipper;
     @Mock
     private GlobalSettings mGlobalSettings;
@@ -102,7 +109,6 @@
         mSecurityViewFlipperLayoutParams = new FrameLayout.LayoutParams(
                 MATCH_PARENT, MATCH_PARENT);
 
-        when(mSecurityViewFlipper.getWindowInsetsController()).thenReturn(mWindowInsetsController);
         when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
         mKeyguardSecurityContainer = new KeyguardSecurityContainer(getContext());
         mKeyguardSecurityContainer.mSecurityViewFlipper = mSecurityViewFlipper;
@@ -212,14 +218,12 @@
         mKeyguardSecurityContainer.updatePositionByTouchX(
                 mKeyguardSecurityContainer.getWidth() - 1f);
 
-        verify(mGlobalSettings).putInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
-                Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
-        verify(mSecurityViewFlipper).setTranslationX(
+        verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_RIGHT);
+        assertSecurityTranslationX(
                 mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
 
         mKeyguardSecurityContainer.updatePositionByTouchX(1f);
-        verify(mGlobalSettings).putInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
-                Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT);
+        verify(mGlobalSettings).putInt(ONE_HANDED_KEYGUARD_SIDE, ONE_HANDED_KEYGUARD_SIDE_LEFT);
 
         verify(mSecurityViewFlipper).setTranslationX(0.0f);
     }
@@ -237,49 +241,43 @@
     }
 
     @Test
-    public void testUserSwitcherModeViewGravityLandscape() {
+    public void testUserSwitcherModeViewPositionLandscape() {
         // GIVEN one user has been setup and in landscape
         when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1));
-        Configuration config = new Configuration();
-        config.orientation = Configuration.ORIENTATION_LANDSCAPE;
-        when(getContext().getResources().getConfiguration()).thenReturn(config);
+        Configuration landscapeConfig = configuration(ORIENTATION_LANDSCAPE);
+        when(getContext().getResources().getConfiguration()).thenReturn(landscapeConfig);
 
         // WHEN UserSwitcherViewMode is initialized and config has changed
         setupUserSwitcher();
-        reset(mSecurityViewFlipper);
-        when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
-        mKeyguardSecurityContainer.onConfigurationChanged(config);
+        mKeyguardSecurityContainer.onConfigurationChanged(landscapeConfig);
 
         // THEN views are oriented side by side
-        verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture());
-        assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.RIGHT | Gravity.BOTTOM);
-        ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
-                R.id.keyguard_bouncer_user_switcher);
-        assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity)
-                .isEqualTo(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+        assertSecurityGravity(Gravity.LEFT | Gravity.BOTTOM);
+        assertUserSwitcherGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+        assertSecurityTranslationX(
+                mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
+        assertUserSwitcherTranslationX(0f);
+
     }
 
     @Test
     public void testUserSwitcherModeViewGravityPortrait() {
         // GIVEN one user has been setup and in landscape
         when(mUserSwitcherController.getUsers()).thenReturn(buildUserRecords(1));
-        Configuration config = new Configuration();
-        config.orientation = Configuration.ORIENTATION_PORTRAIT;
-        when(getContext().getResources().getConfiguration()).thenReturn(config);
+        Configuration portraitConfig = configuration(ORIENTATION_PORTRAIT);
+        when(getContext().getResources().getConfiguration()).thenReturn(portraitConfig);
 
         // WHEN UserSwitcherViewMode is initialized and config has changed
         setupUserSwitcher();
         reset(mSecurityViewFlipper);
         when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
-        mKeyguardSecurityContainer.onConfigurationChanged(config);
+        mKeyguardSecurityContainer.onConfigurationChanged(portraitConfig);
 
         // THEN views are both centered horizontally
-        verify(mSecurityViewFlipper).setLayoutParams(mLayoutCaptor.capture());
-        assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(Gravity.CENTER_HORIZONTAL);
-        ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
-                R.id.keyguard_bouncer_user_switcher);
-        assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity)
-                .isEqualTo(Gravity.CENTER_HORIZONTAL);
+        assertSecurityGravity(Gravity.CENTER_HORIZONTAL);
+        assertUserSwitcherGravity(Gravity.CENTER_HORIZONTAL);
+        assertSecurityTranslationX(0);
+        assertUserSwitcherTranslationX(0);
     }
 
     @Test
@@ -310,9 +308,102 @@
         assertThat(anchor.isClickable()).isTrue();
     }
 
+    @Test
+    public void testTouchesAreRecognizedAsBeingOnTheOtherSideOfSecurity() {
+        setupUserSwitcher();
+        setViewWidth(VIEW_WIDTH);
+
+        // security is on the right side by default
+        assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
+                touchEventLeftSide())).isTrue();
+        assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
+                touchEventRightSide())).isFalse();
+
+        // move security to the left side
+        when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT);
+        mKeyguardSecurityContainer.onConfigurationChanged(new Configuration());
+
+        assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
+                touchEventLeftSide())).isFalse();
+        assertThat(mKeyguardSecurityContainer.isTouchOnTheOtherSideOfSecurity(
+                touchEventRightSide())).isTrue();
+    }
+
+    @Test
+    public void testSecuritySwitchesSidesInLandscapeUserSwitcherMode() {
+        when(getContext().getResources().getConfiguration())
+                .thenReturn(configuration(ORIENTATION_LANDSCAPE));
+        setupUserSwitcher();
+
+        // switch sides
+        when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_LEFT);
+        mKeyguardSecurityContainer.onConfigurationChanged(new Configuration());
+
+        assertSecurityTranslationX(0);
+        assertUserSwitcherTranslationX(
+                mKeyguardSecurityContainer.getWidth() - mSecurityViewFlipper.getWidth());
+    }
+
+    private Configuration configuration(@Configuration.Orientation int orientation) {
+        Configuration config = new Configuration();
+        config.orientation = orientation;
+        return config;
+    }
+
+    private void assertSecurityTranslationX(float translation) {
+        verify(mSecurityViewFlipper).setTranslationX(translation);
+    }
+
+    private void assertUserSwitcherTranslationX(float translation) {
+        ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
+                R.id.keyguard_bouncer_user_switcher);
+        assertThat(userSwitcher.getTranslationX()).isEqualTo(translation);
+    }
+
+    private void assertUserSwitcherGravity(@Gravity.GravityFlags int gravity) {
+        ViewGroup userSwitcher = mKeyguardSecurityContainer.findViewById(
+                R.id.keyguard_bouncer_user_switcher);
+        assertThat(((FrameLayout.LayoutParams) userSwitcher.getLayoutParams()).gravity)
+                .isEqualTo(gravity);
+    }
+
+    private void assertSecurityGravity(@Gravity.GravityFlags int gravity) {
+        verify(mSecurityViewFlipper, atLeastOnce()).setLayoutParams(mLayoutCaptor.capture());
+        assertThat(mLayoutCaptor.getValue().gravity).isEqualTo(gravity);
+    }
+
+    private void setViewWidth(int width) {
+        mKeyguardSecurityContainer.setRight(width);
+        mKeyguardSecurityContainer.setLeft(0);
+    }
+
+    private MotionEvent touchEventLeftSide() {
+        return MotionEvent.obtain(
+                /* downTime= */0,
+                /* eventTime= */0,
+                MotionEvent.ACTION_DOWN,
+                /* x= */VIEW_WIDTH / 3f,
+                /* y= */0,
+                /* metaState= */0);
+    }
+
+    private MotionEvent touchEventRightSide() {
+        return MotionEvent.obtain(
+                /* downTime= */0,
+                /* eventTime= */0,
+                MotionEvent.ACTION_DOWN,
+                /* x= */(VIEW_WIDTH / 3f) * 2,
+                /* y= */0,
+                /* metaState= */0);
+    }
+
     private void setupUserSwitcher() {
+        when(mGlobalSettings.getInt(any(), anyInt())).thenReturn(ONE_HANDED_KEYGUARD_SIDE_RIGHT);
         mKeyguardSecurityContainer.initMode(KeyguardSecurityContainer.MODE_USER_SWITCHER,
                 mGlobalSettings, mFalsingManager, mUserSwitcherController);
+        // reset mSecurityViewFlipper so setup doesn't influence test verifications
+        reset(mSecurityViewFlipper);
+        when(mSecurityViewFlipper.getLayoutParams()).thenReturn(mSecurityViewFlipperLayoutParams);
     }
 
     private ArrayList<UserRecord> buildUserRecords(int count) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index a35efa9..90609fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -210,7 +210,8 @@
                 BOUNDS_POSITION_TOP,
                 mAuthController,
                 mStatusBarStateController,
-                mKeyguardUpdateMonitor));
+                mKeyguardUpdateMonitor,
+                mExecutor));
 
         mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings,
                 mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
index d5df9fe..c48cbb1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt
@@ -159,7 +159,7 @@
     @Test
     fun doesNotStartIfAnimationIsCancelled() {
         val runner = activityLaunchAnimator.createRunner(controller)
-        runner.onAnimationCancelled()
+        runner.onAnimationCancelled(false /* isKeyguardOccluded */)
         runner.onAnimationStart(0, emptyArray(), emptyArray(), emptyArray(), iCallback)
 
         waitForIdleSync()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index 0fdd905..b61bda8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -21,6 +21,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -468,6 +469,40 @@
         verify(mView).setUnpausedAlpha(255);
     }
 
+    @Test
+    public void testUpdatePanelExpansion_pauseAuth() {
+        // GIVEN view is attached + on the keyguard
+        mController.onViewAttached();
+        captureStatusBarStateListeners();
+        captureStatusBarExpansionListeners();
+        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
+        reset(mView);
+
+        // WHEN panelViewExpansion changes to hide
+        when(mView.getUnpausedAlpha()).thenReturn(0);
+        updateStatusBarExpansion(0f, false);
+
+        // THEN pause auth is updated to PAUSE
+        verify(mView, atLeastOnce()).setPauseAuth(true);
+    }
+
+    @Test
+    public void testUpdatePanelExpansion_unpauseAuth() {
+        // GIVEN view is attached + on the keyguard + panel expansion is 0f
+        mController.onViewAttached();
+        captureStatusBarStateListeners();
+        captureStatusBarExpansionListeners();
+        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
+        reset(mView);
+
+        // WHEN panelViewExpansion changes to expanded
+        when(mView.getUnpausedAlpha()).thenReturn(255);
+        updateStatusBarExpansion(1f, true);
+
+        // THEN pause auth is updated to NOT pause
+        verify(mView, atLeastOnce()).setPauseAuth(false);
+    }
+
     private void sendStatusBarStateChanged(int statusBarState) {
         mStatusBarStateListener.onStateChanged(statusBarState);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index fb64c7b..2adf285 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -207,4 +207,15 @@
             assertThat(complications.contains(weatherComplication)).isFalse();
         }
     }
+
+    @Test
+    public void testComplicationWithNoTypeNotFiltered() {
+        final Complication complication = Mockito.mock(Complication.class);
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor);
+        stateController.addComplication(complication);
+        mExecutor.runAllReady();
+        assertThat(stateController.getComplications(true).contains(complication))
+                .isTrue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java
index dc1ae0e..964e6d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java
@@ -72,19 +72,67 @@
     }
 
     /**
-     * Ensures {@link SmartSpaceComplication} is only registered when it is available.
+     * Ensures {@link SmartSpaceComplication} isn't registered right away on start.
      */
     @Test
-    public void testAvailability() {
+    public void testRegistrantStart_doesNotAddComplication() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+        verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication));
+    }
 
-        final SmartSpaceComplication.Registrant registrant = new SmartSpaceComplication.Registrant(
+    private SmartSpaceComplication.Registrant getRegistrant() {
+        return new SmartSpaceComplication.Registrant(
                 mContext,
                 mDreamOverlayStateController,
                 mComplication,
                 mSmartspaceController);
-        registrant.start();
-        verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication));
+    }
 
+    @Test
+    public void testOverlayActive_addsTargetListener() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+
+        final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture());
+
+        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
+        dreamCallbackCaptor.getValue().onStateChanged();
+
+        // Test
+        final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor =
+                ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class);
+        verify(mSmartspaceController).addListener(listenerCaptor.capture());
+    }
+
+    @Test
+    public void testOverlayActive_targetsNonEmpty_addsComplication() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+
+        final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture());
+
+        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
+        dreamCallbackCaptor.getValue().onStateChanged();
+
+        final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor =
+                ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class);
+        verify(mSmartspaceController).addListener(listenerCaptor.capture());
+
+        // Test
+        final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class);
+        listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target));
+        verify(mDreamOverlayStateController).addComplication(eq(mComplication));
+    }
+
+    @Test
+    public void testOverlayActive_targetsEmpty_removesComplication() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
 
         final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor =
                 ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
@@ -100,10 +148,41 @@
         final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class);
         listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target));
         verify(mDreamOverlayStateController).addComplication(eq(mComplication));
+
+        // Test
+        listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList());
+        verify(mDreamOverlayStateController).removeComplication(eq(mComplication));
     }
 
     @Test
-    public void testGetViewReusesSameView() {
+    public void testOverlayInActive_removesTargetListener_removesComplication() {
+        final SmartSpaceComplication.Registrant registrant = getRegistrant();
+        registrant.start();
+
+        final ArgumentCaptor<DreamOverlayStateController.Callback> dreamCallbackCaptor =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture());
+
+        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
+        dreamCallbackCaptor.getValue().onStateChanged();
+
+        final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> listenerCaptor =
+                ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class);
+        verify(mSmartspaceController).addListener(listenerCaptor.capture());
+
+        final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class);
+        listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target));
+        verify(mDreamOverlayStateController).addComplication(eq(mComplication));
+
+        // Test
+        when(mDreamOverlayStateController.isOverlayActive()).thenReturn(false);
+        dreamCallbackCaptor.getValue().onStateChanged();
+        verify(mSmartspaceController).removeListener(listenerCaptor.getValue());
+        verify(mDreamOverlayStateController).removeComplication(eq(mComplication));
+    }
+
+    @Test
+    public void testGetView_reusesSameView() {
         final SmartSpaceComplication complication = new SmartSpaceComplication(getContext(),
                 mSmartspaceController);
         final Complication.ViewHolder viewHolder = complication.createView(mComplicationViewModel);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 11326e7..59475cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,6 +52,8 @@
     private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
     private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
     private static final String TEST_SESSION_NAME = "test_session_name";
+    private static final int TEST_MAX_VOLUME = 20;
+    private static final int TEST_CURRENT_VOLUME = 10;
 
     // Mock
     private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
@@ -64,12 +67,14 @@
     private MediaOutputAdapter mMediaOutputAdapter;
     private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
     private List<MediaDevice> mMediaDevices = new ArrayList<>();
+    MediaOutputSeekbar mSpyMediaOutputSeekbar;
 
     @Before
     public void setUp() {
         mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController, mMediaOutputDialog);
         mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
                 .onCreateViewHolder(new LinearLayout(mContext), 0);
+        mSpyMediaOutputSeekbar = spy(mViewHolder.mSeekBar);
 
         when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
         when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
@@ -169,6 +174,16 @@
     }
 
     @Test
+    public void onBindViewHolder_initSeekbar_setsVolume() {
+        when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
+        when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME);
+    }
+
+    @Test
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index e3b5059..9eaa20c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -32,6 +32,7 @@
 import android.media.session.MediaSessionManager;
 import android.media.session.PlaybackState;
 import android.os.Bundle;
+import android.os.PowerExemptionManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -86,6 +87,7 @@
             NearbyMediaDevicesManager.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
+    private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
@@ -110,7 +112,7 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotificationEntryManager, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
         mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext, mBroadcastSender,
                 mMediaOutputController);
         mMediaOutputBaseDialogImpl.onCreate(new Bundle());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index feed334..2bf5f0f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -19,6 +19,9 @@
 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.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -34,10 +37,12 @@
 import android.media.AudioManager;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
+import android.media.MediaRoute2Info;
 import android.media.NearbyDevice;
 import android.media.RoutingSessionInfo;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
+import android.os.PowerExemptionManager;
 import android.os.RemoteException;
 import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
@@ -97,6 +102,7 @@
     private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
     private ActivityStarter mStarter = mock(ActivityStarter.class);
     private AudioManager mAudioManager = mock(AudioManager.class);
+    private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
     private CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
@@ -125,7 +131,7 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
         mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         MediaDescription.Builder builder = new MediaDescription.Builder();
@@ -177,7 +183,7 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
 
         mMediaOutputController.start(mCb);
 
@@ -206,7 +212,7 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
 
         mMediaOutputController.start(mCb);
 
@@ -511,7 +517,7 @@
         mMediaOutputController = new MediaOutputController(mSpyContext, null,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotifCollection, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
 
         assertThat(mMediaOutputController.getNotificationIcon()).isNull();
     }
@@ -591,4 +597,20 @@
 
         assertThat(mMediaOutputController.isVolumeControlEnabled(mMediaDevice1)).isTrue();
     }
+
+    @Test
+    public void setTemporaryAllowListExceptionIfNeeded_fromRemoteToBluetooth_addsAllowList() {
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
+        when(mMediaDevice1.getFeatures()).thenReturn(
+                ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK));
+        when(mMediaDevice2.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+
+        mMediaOutputController.setTemporaryAllowListExceptionIfNeeded(mMediaDevice2);
+
+        verify(mPowerExemptionManager).addToTemporaryAllowList(anyString(), anyInt(), anyString(),
+                anyLong());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index b16c928..c45db05 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -29,6 +29,7 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
 import android.media.session.PlaybackState;
+import android.os.PowerExemptionManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -84,6 +85,7 @@
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
+    private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
 
     private List<MediaController> mMediaControllers = new ArrayList<>();
     private MediaOutputDialog mMediaOutputDialog;
@@ -103,7 +105,7 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotificationEntryManager, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
                 mMediaOutputController, mUiEventLogger);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
index 379bb4f..4534ae6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputGroupDialogTest.java
@@ -23,6 +23,7 @@
 
 import android.media.AudioManager;
 import android.media.session.MediaSessionManager;
+import android.os.PowerExemptionManager;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -70,6 +71,7 @@
     private NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
+    private PowerExemptionManager mPowerExemptionManager = mock(PowerExemptionManager.class);
 
     private MediaOutputGroupDialog mMediaOutputGroupDialog;
     private MediaOutputController mMediaOutputController;
@@ -80,7 +82,7 @@
         mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
                 mMediaSessionManager, mLocalBluetoothManager, mStarter,
                 mNotificationEntryManager, mDialogLaunchAnimator,
-                Optional.of(mNearbyMediaDevicesManager), mAudioManager);
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager);
         mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
         mMediaOutputGroupDialog = new MediaOutputGroupDialog(mContext, false, mBroadcastSender,
                 mMediaOutputController);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
index b9a69bb..2eb4783 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
@@ -25,6 +25,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
@@ -65,6 +66,8 @@
     @Mock
     private lateinit var logger: MediaTttLogger
     @Mock
+    private lateinit var accessibilityManager: AccessibilityManager
+    @Mock
     private lateinit var windowManager: WindowManager
     @Mock
     private lateinit var viewUtil: ViewUtil
@@ -88,11 +91,21 @@
         )).thenReturn(applicationInfo)
         context.setMockPackageManager(packageManager)
 
+        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any()))
+            .thenReturn(TIMEOUT_MS.toInt())
+
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
 
         controllerCommon = TestControllerCommon(
-            context, logger, windowManager, viewUtil, fakeExecutor, tapGestureDetector, powerManager
+            context,
+            logger,
+            windowManager,
+            viewUtil,
+            fakeExecutor,
+            accessibilityManager,
+            tapGestureDetector,
+            powerManager
         )
     }
 
@@ -344,6 +357,7 @@
         windowManager: WindowManager,
         viewUtil: ViewUtil,
         @Main mainExecutor: DelayableExecutor,
+        accessibilityManager: AccessibilityManager,
         tapGestureDetector: TapGestureDetector,
         powerManager: PowerManager
     ) : MediaTttChipControllerCommon<ChipInfo>(
@@ -352,23 +366,22 @@
         windowManager,
         viewUtil,
         mainExecutor,
+        accessibilityManager,
         tapGestureDetector,
         powerManager,
         R.layout.media_ttt_chip
     ) {
-        override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {
-
-        }
-
-        override fun getIconSize(isAppIcon: Boolean): Int? = ICON_SIZE
+        override val windowLayoutParams = commonWindowLayoutParams
+        override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {}
+        override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE
     }
 
     inner class ChipInfo : ChipInfoCommon {
-        override fun getTimeoutMs() = TIMEOUT_MS
+        override fun getTimeoutMs() = 1L
     }
 }
 
 private const val PACKAGE_NAME = "com.android.systemui"
 private const val APP_NAME = "Fake App Name"
 private const val TIMEOUT_MS = 10000L
-private const val ICON_SIZE = 47
\ No newline at end of file
+private const val ICON_SIZE = 47
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 9edc4f4..bbc5641 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -28,6 +28,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
@@ -65,6 +66,8 @@
     @Mock
     private lateinit var logger: MediaTttLogger
     @Mock
+    private lateinit var accessibilityManager: AccessibilityManager
+    @Mock
     private lateinit var powerManager: PowerManager
     @Mock
     private lateinit var windowManager: WindowManager
@@ -99,6 +102,7 @@
             windowManager,
             viewUtil,
             FakeExecutor(FakeSystemClock()),
+            accessibilityManager,
             TapGestureDetector(context),
             powerManager,
             Handler.getMain(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index a8c72dd..7ca0cd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -27,6 +27,7 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
 import android.widget.ImageView
 import android.widget.TextView
 import androidx.test.filters.SmallTest
@@ -67,6 +68,8 @@
     @Mock
     private lateinit var logger: MediaTttLogger
     @Mock
+    private lateinit var accessibilityManager: AccessibilityManager
+    @Mock
     private lateinit var powerManager: PowerManager
     @Mock
     private lateinit var windowManager: WindowManager
@@ -95,9 +98,12 @@
 
         fakeClock = FakeSystemClock()
         fakeExecutor = FakeExecutor(fakeClock)
+
         uiEventLoggerFake = UiEventLoggerFake()
         senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
 
+        whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+
         controllerSender = MediaTttChipControllerSender(
             commandQueue,
             context,
@@ -105,6 +111,7 @@
             windowManager,
             viewUtil,
             fakeExecutor,
+            accessibilityManager,
             TapGestureDetector(context),
             powerManager,
             senderUiEventLogger
@@ -592,7 +599,7 @@
         fakeClock.advanceTime(1000L)
         controllerSender.removeChip("fakeRemovalReason")
 
-        fakeClock.advanceTime(state.state.timeout + 1)
+        fakeClock.advanceTime(TIMEOUT + 1L)
 
         verify(windowManager).removeView(any())
     }
@@ -615,7 +622,7 @@
         fakeClock.advanceTime(1000L)
         controllerSender.removeChip("fakeRemovalReason")
 
-        fakeClock.advanceTime(state.state.timeout + 1)
+        fakeClock.advanceTime(TIMEOUT + 1L)
 
         verify(windowManager).removeView(any())
     }
@@ -674,6 +681,7 @@
 private const val APP_NAME = "Fake app name"
 private const val OTHER_DEVICE_NAME = "My Tablet"
 private const val PACKAGE_NAME = "com.android.systemui"
+private const val TIMEOUT = 10000
 
 private val routeInfo = MediaRoute2Info.Builder("id", OTHER_DEVICE_NAME)
     .addFeature("feature")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
index 489c8c8..bf237ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
@@ -57,6 +57,7 @@
 
     @Test
     fun testContainerBottomPadding() {
+        val originalPadding = qsPanelContainer.paddingBottom
         qsContainer.updateResources(
             qsPanelController,
             quickStatusBarHeaderController
@@ -66,7 +67,7 @@
                 anyInt(),
                 anyInt(),
                 anyInt(),
-                eq(mContext.resources.getDimensionPixelSize(R.dimen.footer_actions_height))
+                eq(originalPadding)
             )
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 60cfd72..b98be75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -150,6 +150,14 @@
         assertThat(footer.isVisibleToUser).isTrue()
     }
 
+    @Test
+    fun testBottomPadding() {
+        val padding = 10
+        context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_bottom, padding)
+        qsPanel.updatePadding()
+        assertThat(qsPanel.paddingBottom).isEqualTo(padding)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
index 2f0f0a0..37f96c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt
@@ -36,17 +36,17 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.withArgCaptor
 import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Spy
 import org.mockito.Mockito
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
-import java.util.Optional
-import java.util.concurrent.Executor
+import org.mockito.Spy
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -87,6 +87,34 @@
 
     private lateinit var controller: DreamSmartspaceController
 
+    /**
+     * A class which implements SmartspaceView and extends View. This is mocked to provide the right
+     * object inheritance and interface implementation used in DreamSmartspaceController
+     */
+    private class TestView(context: Context?) : View(context), SmartspaceView {
+        override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {}
+
+        override fun setPrimaryTextColor(color: Int) {}
+
+        override fun setIsDreaming(isDreaming: Boolean) {}
+
+        override fun setDozeAmount(amount: Float) {}
+
+        override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {}
+
+        override fun setFalsingManager(falsingManager: FalsingManager?) {}
+
+        override fun setDnd(image: Drawable?, description: String?) {}
+
+        override fun setNextAlarm(image: Drawable?, description: String?) {}
+
+        override fun setMediaTarget(target: SmartspaceTarget?) {}
+
+        override fun getSelectedPage(): Int { return 0; }
+
+        override fun getCurrentCardTopPadding(): Int { return 0; }
+    }
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -130,34 +158,6 @@
     }
 
     /**
-     * A class which implements SmartspaceView and extends View. This is mocked to provide the right
-     * object inheritance and interface implementation used in DreamSmartspaceController
-     */
-    private class TestView(context: Context?) : View(context), SmartspaceView {
-        override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) {}
-
-        override fun setPrimaryTextColor(color: Int) {}
-
-        override fun setIsDreaming(isDreaming: Boolean) {}
-
-        override fun setDozeAmount(amount: Float) {}
-
-        override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) {}
-
-        override fun setFalsingManager(falsingManager: FalsingManager?) {}
-
-        override fun setDnd(image: Drawable?, description: String?) {}
-
-        override fun setNextAlarm(image: Drawable?, description: String?) {}
-
-        override fun setMediaTarget(target: SmartspaceTarget?) {}
-
-        override fun getSelectedPage(): Int { return 0; }
-
-        override fun getCurrentCardTopPadding(): Int { return 0; }
-    }
-
-    /**
      * Ensures session begins when a view is attached.
      */
     @Test
@@ -180,16 +180,4 @@
 
         verify(session).close()
     }
-
-    /**
-     * Ensures setIsDreaming(true) is called when the view is built.
-     */
-    @Test
-    fun testSetIsDreamingTrueOnViewCreate() {
-        `when`(precondition.conditionsMet()).thenReturn(true)
-
-        controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java))
-
-        verify(smartspaceView).setIsDreaming(true)
-    }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
index b4cae38..d0cf792 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt
@@ -74,9 +74,9 @@
 
         // Verify ripple added to window manager.
         captor.value.onBatteryLevelChanged(
-                0 /* unusedBatteryLevel */,
-                true /* plugged in */,
-                false /* charging */)
+                /* unusedBatteryLevel= */ 0,
+                /* plugged in= */ true,
+                /* charging= */ false)
         val attachListenerCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
         verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
@@ -144,4 +144,22 @@
         // Verify that ripple is triggered.
         verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any())
     }
+
+    @Test
+    fun testRipple_whenDocked_doesNotPlayRipple() {
+        `when`(batteryController.isChargingSourceDock).thenReturn(true)
+        val captor = ArgumentCaptor
+                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
+        verify(batteryController).addCallback(captor.capture())
+
+        captor.value.onBatteryLevelChanged(
+                /* unusedBatteryLevel= */ 0,
+                /* plugged in= */ true,
+                /* charging= */ false)
+
+        val attachListenerCaptor =
+                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
+        verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture())
+        verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index 7d06abf..3fc0c81 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -63,7 +63,7 @@
                 mock(StatusBarKeyguardViewManager.class));
         mDynamicPrivacyController.addListener(mListener);
         // Disable dynamic privacy by default
-        allowPrivateNotificationsInPublic(true);
+        allowNotificationsInPublic(false);
     }
 
     @Test
@@ -108,24 +108,21 @@
 
     @Test
     public void dynamicPrivacyOnlyWhenHidingPrivate() {
-        // Verify that when only hiding notifications, this isn't enabled
-        allowPrivateNotificationsInPublic(true);
-        when(mLockScreenUserManager.shouldHideNotifications(any())).thenReturn(
-                false);
-        assertFalse("Dynamic privacy shouldn't be enabled when only hiding notifications",
+        // Verify that when hiding notifications, this isn't enabled
+        allowNotificationsInPublic(false);
+        assertFalse("Dynamic privacy shouldn't be enabled when hiding notifications",
                 mDynamicPrivacyController.isDynamicPrivacyEnabled());
-        allowPrivateNotificationsInPublic(false);
-        assertTrue("Should be enabled when hiding notification contents",
+        allowNotificationsInPublic(true);
+        assertTrue("Should be enabled whenever notifications are visible",
                 mDynamicPrivacyController.isDynamicPrivacyEnabled());
     }
 
     private void enableDynamicPrivacy() {
-        allowPrivateNotificationsInPublic(false);
+        allowNotificationsInPublic(true);
     }
 
-    private void allowPrivateNotificationsInPublic(boolean allow) {
-        when(mLockScreenUserManager.userAllowsPrivateNotificationsInPublic(anyInt())).thenReturn(
-                allow);
+    private void allowNotificationsInPublic(boolean allow) {
+        when(mLockScreenUserManager.userAllowsNotificationsInPublic(anyInt())).thenReturn(allow);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
index 4507366..ee7d558 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
@@ -85,6 +85,11 @@
         mRankings.put(key, ranking);
     }
 
+    /** This is for testing error cases: b/216384850 */
+    public Ranking removeRankingWithoutEvent(String key) {
+        return mRankings.remove(key);
+    }
+
     private RankingMap buildRankingMap() {
         return new RankingMap(mRankings.values().toArray(new Ranking[0]));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 958d542..f286349 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1492,6 +1492,80 @@
     }
 
     @Test
+    public void testMissingRankingWhenRemovalFeatureIsDisabled() {
+        // GIVEN a pipeline with one two notifications
+        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
+        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
+        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
+        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
+        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
+        clearInvocations(mCollectionListener);
+
+        // GIVEN the message for removing key1 gets does not reach NotifCollection
+        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
+        // WHEN the message for removing key2 arrives
+        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
+
+        // THEN only entry2 gets removed
+        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
+        verify(mLogger, never()).logRecoveredRankings(any());
+        clearInvocations(mCollectionListener, mLogger);
+
+        // WHEN a ranking update includes key1 again
+        mNoMan.setRanking(key1, ranking1);
+        mNoMan.issueRankingUpdate();
+
+        // VERIFY that we do nothing but log the 'recovery'
+        verify(mCollectionListener).onRankingUpdate(any());
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+    }
+
+    @Test
+    public void testMissingRankingWhenRemovalFeatureIsEnabled() {
+        // GIVEN a pipeline with one two notifications
+        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
+        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
+        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
+        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
+        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
+        clearInvocations(mCollectionListener);
+
+        // GIVEN the message for removing key1 gets does not reach NotifCollection
+        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
+        // WHEN the message for removing key2 arrives
+        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
+
+        // THEN both entry1 and entry2 get removed
+        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
+        verify(mCollectionListener).onEntryRemoved(eq(entry1), eq(REASON_UNKNOWN));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry1));
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
+        verify(mLogger, never()).logRecoveredRankings(any());
+        clearInvocations(mCollectionListener, mLogger);
+
+        // WHEN a ranking update includes key1 again
+        mNoMan.setRanking(key1, ranking1);
+        mNoMan.issueRankingUpdate();
+
+        // VERIFY that we do nothing but log the 'recovery'
+        verify(mCollectionListener).onRankingUpdate(any());
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+    }
+
+    @Test
     public void testRegisterFutureDismissal() throws RemoteException {
         // GIVEN a pipeline with one notification
         NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
index 769143d..d4add75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -108,6 +108,7 @@
     @Test
     public void testBlockableEntryWhenCritical() {
         doReturn(true).when(mChannel).isBlockable();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertTrue(mEntry.isBlockable());
     }
@@ -117,6 +118,7 @@
     public void testBlockableEntryWhenCriticalAndChannelNotBlockable() {
         doReturn(true).when(mChannel).isBlockable();
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertTrue(mEntry.isBlockable());
     }
@@ -125,6 +127,7 @@
     public void testNonBlockableEntryWhenCriticalAndChannelNotBlockable() {
         doReturn(false).when(mChannel).isBlockable();
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
+        mEntry.setRanking(mEntry.getRanking());
 
         assertFalse(mEntry.isBlockable());
     }
@@ -164,6 +167,9 @@
         doReturn(true).when(mChannel).isImportanceLockedByCriticalDeviceFunction();
         doReturn(false).when(mChannel).isBlockable();
 
+        mEntry.setRanking(mEntry.getRanking());
+
+        assertFalse(mEntry.isBlockable());
         assertTrue(mEntry.isExemptFromDndVisualSuppression());
         assertFalse(mEntry.shouldSuppressAmbient());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 4e7e79f..9546058 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -53,6 +53,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.NotificationInteractionTracker;
+import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder.OnRenderListListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
@@ -1797,6 +1798,7 @@
 
     @Test
     public void testStableMultipleSectionOrdering() {
+        // WHEN the list is originally built with reordering disabled
         mListBuilder.setSectioners(asList(
                 new PackageSectioner(PACKAGE_1), new PackageSectioner(PACKAGE_2)));
         mStabilityManager.setAllowEntryReordering(false);
@@ -1807,12 +1809,94 @@
         addNotif(3, PACKAGE_1).setRank(3);
         dispatchBuild();
 
+        // VERIFY the order and that entry reordering has not been suppressed
         verifyBuiltList(
                 notif(0),
                 notif(1),
                 notif(3),
                 notif(2)
         );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the ranks change
+        setNewRank(notif(0).entry, 4);
+        dispatchBuild();
+
+        // VERIFY the order does not change that entry reordering has been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(3),
+                notif(2)
+        );
+        verify(mStabilityManager).onEntryReorderSuppressed();
+
+        // WHEN reordering is now allowed again
+        mStabilityManager.setAllowEntryReordering(true);
+        dispatchBuild();
+
+        // VERIFY that list order changes
+        verifyBuiltList(
+                notif(1),
+                notif(3),
+                notif(0),
+                notif(2)
+        );
+    }
+
+    @Test
+    public void testStableChildOrdering() {
+        // WHEN the list is originally built with reordering disabled
+        mStabilityManager.setAllowEntryReordering(false);
+        addGroupSummary(0, PACKAGE_1, GROUP_1).setRank(0);
+        addGroupChild(1, PACKAGE_1, GROUP_1).setRank(1);
+        addGroupChild(2, PACKAGE_1, GROUP_1).setRank(2);
+        addGroupChild(3, PACKAGE_1, GROUP_1).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(2),
+                        child(3)
+                )
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the ranks change
+        setNewRank(notif(2).entry, 5);
+        dispatchBuild();
+
+        // VERIFY the order does not change that entry reordering has been suppressed
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(2),
+                        child(3)
+                )
+        );
+        verify(mStabilityManager).onEntryReorderSuppressed();
+
+        // WHEN reordering is now allowed again
+        mStabilityManager.setAllowEntryReordering(true);
+        dispatchBuild();
+
+        // VERIFY that list order changes
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(3),
+                        child(2)
+                )
+        );
+    }
+
+    private static void setNewRank(NotificationEntry entry, int rank) {
+        entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java
deleted file mode 100644
index d082d74..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.coordinator;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.os.Handler;
-import android.os.UserHandle;
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider;
-import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
-import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-/**
- * TODO(b/224771204) Create test cases
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@Ignore
-public class KeyguardCoordinatorTest extends SysuiTestCase {
-    private static final int NOTIF_USER_ID = 0;
-    private static final int CURR_USER_ID = 1;
-
-    @Mock private Handler mMainHandler;
-    @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private BroadcastDispatcher mBroadcastDispatcher;
-    @Mock private StatusBarStateController mStatusBarStateController;
-    @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock private HighPriorityProvider mHighPriorityProvider;
-    @Mock private SectionHeaderVisibilityProvider mSectionHeaderVisibilityProvider;
-    @Mock private NotifPipeline mNotifPipeline;
-    @Mock private KeyguardNotificationVisibilityProvider mKeyguardNotificationVisibilityProvider;
-
-    private NotificationEntry mEntry;
-    private NotifFilter mKeyguardFilter;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        KeyguardCoordinator keyguardCoordinator = new KeyguardCoordinator(
-                mStatusBarStateController,
-                mKeyguardUpdateMonitor, mHighPriorityProvider, mSectionHeaderVisibilityProvider,
-                mKeyguardNotificationVisibilityProvider, mock(SharedCoordinatorLogger.class));
-
-        mEntry = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .build();
-
-        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
-        keyguardCoordinator.attach(mNotifPipeline);
-        verify(mNotifPipeline, times(1)).addFinalizeFilter(filterCaptor.capture());
-        mKeyguardFilter = filterCaptor.getValue();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
new file mode 100644
index 0000000..8c506a6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
+import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
+import java.util.function.Consumer
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class KeyguardCoordinatorTest : SysuiTestCase() {
+    private val notifPipeline: NotifPipeline = mock()
+    private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
+    private val sectionHeaderVisibilityProvider: SectionHeaderVisibilityProvider = mock()
+    private val sharedCoordinatorLogger: SharedCoordinatorLogger = mock()
+    private val statusBarStateController: StatusBarStateController = mock()
+
+    private lateinit var onStateChangeListener: Consumer<String>
+    private lateinit var keyguardFilter: NotifFilter
+
+    @Before
+    fun setup() {
+        val keyguardCoordinator = KeyguardCoordinator(
+            keyguardNotifVisibilityProvider,
+            sectionHeaderVisibilityProvider,
+            sharedCoordinatorLogger,
+            statusBarStateController
+        )
+        keyguardCoordinator.attach(notifPipeline)
+        onStateChangeListener = withArgCaptor {
+            verify(keyguardNotifVisibilityProvider).addOnStateChangedListener(capture())
+        }
+        keyguardFilter = withArgCaptor {
+            verify(notifPipeline).addFinalizeFilter(capture())
+        }
+    }
+
+    @Test
+    fun testSetSectionHeadersVisibleInShade() {
+        clearInvocations(sectionHeaderVisibilityProvider)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        onStateChangeListener.accept("state change")
+        verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(true)
+    }
+
+    @Test
+    fun testSetSectionHeadersNotVisibleOnKeyguard() {
+        clearInvocations(sectionHeaderVisibilityProvider)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        onStateChangeListener.accept("state change")
+        verify(sectionHeaderVisibilityProvider).sectionHeadersVisible = eq(false)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index 72d8ff3..a6d3719 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -31,6 +31,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.os.Handler;
 import android.os.RemoteException;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -42,7 +43,6 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.RankingBuilder;
-import com.android.systemui.statusbar.notification.SectionClassifier;
 import com.android.systemui.statusbar.notification.collection.GroupEntry;
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
@@ -57,8 +57,10 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
 import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -97,8 +99,10 @@
     @Mock private IStatusBarService mService;
     @Mock private BindEventManagerImpl mBindEventManagerImpl;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
+    @Mock private Handler mHandler;
+    @Mock private SecureSettings mSecureSettings;
     @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
-    private final SectionClassifier mSectionClassifier = new SectionClassifier();
+    private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider();
 
     private NotifUiAdjustmentProvider mAdjustmentProvider;
 
@@ -110,8 +114,11 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mAdjustmentProvider =
-            new NotifUiAdjustmentProvider(mLockscreenUserManager, mSectionClassifier);
+        mAdjustmentProvider = new NotifUiAdjustmentProvider(
+                mHandler,
+                mSecureSettings,
+                mLockscreenUserManager,
+                mSectionStyleProvider);
         mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build();
         mInflationError = new Exception(TEST_MESSAGE);
         mErrorManager = new NotifInflationErrorManager();
@@ -495,7 +502,7 @@
     private static final int TEST_MAX_GROUP_DELAY = 100;
 
     private void setSectionIsLowPriority(boolean minimized) {
-        mSectionClassifier.setMinimizedSections(minimized
+        mSectionStyleProvider.setMinimizedSections(minimized
                 ? Collections.singleton(mNotifSection.getSectioner())
                 : Collections.emptyList());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index 15c1cb7..50b3fc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -42,7 +42,6 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SbnBuilder;
-import com.android.systemui.statusbar.notification.SectionClassifier;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -50,6 +49,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
 import com.android.systemui.statusbar.notification.collection.render.NodeController;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 
@@ -70,7 +70,7 @@
 
     @Mock private StatusBarStateController mStatusBarStateController;
     @Mock private HighPriorityProvider mHighPriorityProvider;
-    @Mock private SectionClassifier mSectionClassifier;
+    @Mock private SectionStyleProvider mSectionStyleProvider;
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NodeController mAlertingHeaderController;
     @Mock private NodeController mSilentNodeController;
@@ -94,7 +94,7 @@
         mRankingCoordinator = new RankingCoordinator(
                 mStatusBarStateController,
                 mHighPriorityProvider,
-                mSectionClassifier,
+                mSectionStyleProvider,
                 mAlertingHeaderController,
                 mSilentHeaderController,
                 mSilentNodeController);
@@ -102,7 +102,7 @@
         mEntry.setRanking(getRankingForUnfilteredNotif().build());
 
         mRankingCoordinator.attach(mNotifPipeline);
-        verify(mSectionClassifier).setMinimizedSections(any());
+        verify(mSectionStyleProvider).setMinimizedSections(any());
         verify(mNotifPipeline, times(2)).addPreGroupFilter(mNotifFilterCaptor.capture());
         mCapturedSuspendedFilter = mNotifFilterCaptor.getAllValues().get(0);
         mCapturedDozingFilter = mNotifFilterCaptor.getAllValues().get(1);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
index 447ba15..3f3de00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAppearanceCoordinatorTest.kt
@@ -21,13 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.AssistantFeedbackController
 import com.android.systemui.statusbar.notification.FeedbackIcon
-import com.android.systemui.statusbar.notification.SectionClassifier
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfterRenderEntryListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
 import com.android.systemui.statusbar.notification.collection.render.NotifRowController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -37,8 +37,8 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations.initMocks
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations.initMocks
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -53,7 +53,7 @@
 
     @Mock private lateinit var pipeline: NotifPipeline
     @Mock private lateinit var assistantFeedbackController: AssistantFeedbackController
-    @Mock private lateinit var sectionClassifier: SectionClassifier
+    @Mock private lateinit var sectionStyleProvider: SectionStyleProvider
 
     @Mock private lateinit var section1: NotifSection
     @Mock private lateinit var section2: NotifSection
@@ -66,7 +66,7 @@
         coordinator = RowAppearanceCoordinator(
             mContext,
             assistantFeedbackController,
-            sectionClassifier
+            sectionStyleProvider
         )
         coordinator.attach(pipeline)
         beforeRenderListListener = withArgCaptor {
@@ -82,8 +82,8 @@
 
     @Test
     fun testSetSystemExpandedOnlyOnFirst() {
-        whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(false)
-        whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(false)
+        whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(false)
+        whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(false)
         beforeRenderListListener.onBeforeRenderList(listOf(entry1, entry2))
         afterRenderEntryListener.onAfterRenderEntry(entry1, controller1)
         verify(controller1).setSystemExpanded(eq(true))
@@ -93,8 +93,8 @@
 
     @Test
     fun testSetSystemExpandedNeverIfMinimized() {
-        whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(true)
-        whenever(sectionClassifier.isMinimizedSection(eq(section1))).thenReturn(true)
+        whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(true)
+        whenever(sectionStyleProvider.isMinimizedSection(eq(section1))).thenReturn(true)
         beforeRenderListListener.onBeforeRenderList(listOf(entry1, entry2))
         afterRenderEntryListener.onAfterRenderEntry(entry1, controller1)
         verify(controller1).setSystemExpanded(eq(false))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
index dd15cae..246943e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
@@ -1,29 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.systemui.statusbar.notification.collection.inflation
 
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.notification.SectionClassifier
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
+import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.inOrder
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper
 class NotifUiAdjustmentProviderTest : SysuiTestCase() {
     private val lockscreenUserManager: NotificationLockscreenUserManager = mock()
-    private val sectionClassifier: SectionClassifier = mock()
+    private val sectionStyleProvider: SectionStyleProvider = mock()
+    private val handler: Handler = mock()
+    private val secureSettings: SecureSettings = mock()
+    private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE)
+    private val dirtyListener: Runnable = mock()
+
+    private val section = NotifSection(mock(), 0)
+    private val entry = NotificationEntryBuilder()
+        .setSection(section)
+        .setParent(GroupEntry.ROOT_ENTRY)
+        .build()
+
+    private lateinit var contentObserver: ContentObserver
 
     private val adjustmentProvider = NotifUiAdjustmentProvider(
+        handler,
+        secureSettings,
         lockscreenUserManager,
-        sectionClassifier,
+        sectionStyleProvider,
     )
 
+    @Before
+    fun setup() {
+        verifyNoMoreInteractions(secureSettings)
+        adjustmentProvider.addDirtyListener(dirtyListener)
+        verify(secureSettings).getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())
+        contentObserver = withArgCaptor {
+            verify(secureSettings).registerContentObserverForUser(
+                eq(SHOW_NOTIFICATION_SNOOZE), capture(), any()
+            )
+        }
+        verifyNoMoreInteractions(secureSettings, dirtyListener)
+    }
+
     @Test
     fun notifLockscreenStateChangeWillNotifDirty() {
         val dirtyListener = mock<Runnable>()
@@ -33,6 +91,35 @@
                 verify(lockscreenUserManager).addNotificationStateChangedListener(capture())
             }
         notifLocksreenStateChangeListener.onNotificationStateChanged()
-        verify(dirtyListener).run();
+        verify(dirtyListener).run()
+    }
+
+    @Test
+    fun additionalAddDoesNotRegisterAgain() {
+        clearInvocations(secureSettings)
+        adjustmentProvider.addDirtyListener(mock())
+        verifyNoMoreInteractions(secureSettings)
+    }
+
+    @Test
+    fun onChangeWillQueryThenNotifyDirty() {
+        contentObserver.onChange(false, listOf(uri), 0, 0)
+        with(inOrder(secureSettings, dirtyListener)) {
+            verify(secureSettings).getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())
+            verify(dirtyListener).run()
+        }
+    }
+
+    @Test
+    fun changingSnoozeChangesProvidedAdjustment() {
+        whenever(secureSettings.getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())).thenReturn(0)
+        val original = adjustmentProvider.calculateAdjustment(entry)
+        assertThat(original.isSnoozeEnabled).isFalse()
+
+        whenever(secureSettings.getInt(eq(SHOW_NOTIFICATION_SNOOZE), any())).thenReturn(1)
+        contentObserver.onChange(false, listOf(uri), 0, 0)
+        val withSnoozing = adjustmentProvider.calculateAdjustment(entry)
+        assertThat(withSnoozing.isSnoozeEnabled).isTrue()
+        assertThat(withSnoozing).isNotEqualTo(original)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
new file mode 100644
index 0000000..6c07174
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.notifcollection
+
+import android.service.notification.NotificationListenerService.RankingMap
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotifCollectionLoggerTest : SysuiTestCase() {
+    private val logger: NotifCollectionLogger = mock()
+    private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build()
+    private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build()
+
+    private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> =
+        entries.associateBy { it.key }
+
+    private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap =
+        RankingMap(entries.map { it.ranking }.toTypedArray())
+
+    @Test
+    fun testMaybeLogInconsistentRankings_logsNewlyInconsistentRanking() {
+        val rankingMap = rankingMapOf(entry1)
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = emptySet(),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMap
+        )
+        verify(logger).logMissingRankings(
+            newlyInconsistentEntries = eq(listOf(entry2)),
+            totalInconsistent = eq(1),
+            rankingMap = eq(rankingMap),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_logsWhenRankingIsAdded() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1, entry2)
+        )
+        verify(logger).logRecoveredRankings(
+            newlyConsistentKeys = eq(listOf(entry2.key)),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun testMaybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() {
+        maybeLogInconsistentRankings(
+            logger = logger,
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
index 0e18658..ac254ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/NodeSpecBuilderTest.kt
@@ -19,7 +19,6 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager
-import com.android.systemui.statusbar.notification.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
 import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -28,6 +27,7 @@
 import com.android.systemui.statusbar.notification.collection.getAttachState
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_PEOPLE
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
@@ -413,4 +413,4 @@
             return nodeController
         }
     }, index)
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
index 0d5a5fe..3f641df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/HeadsUpViewBinderTest.java
@@ -72,32 +72,32 @@
         });
 
         mViewBinder.bindHeadsUpView(mEntry, null);
-        verify(mLogger).startBindingHun(eq("key"));
+        verify(mLogger).startBindingHun(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         callback.get().onBindFinished(mEntry);
-        verify(mLogger).entryBoundSuccessfully(eq("key"));
+        verify(mLogger).entryBoundSuccessfully(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         mViewBinder.bindHeadsUpView(mEntry, null);
-        verify(mLogger).startBindingHun(eq("key"));
+        verify(mLogger).startBindingHun(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         callback.get().onBindFinished(mEntry);
-        verify(mLogger).entryBoundSuccessfully(eq("key"));
+        verify(mLogger).entryBoundSuccessfully(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         mViewBinder.unbindHeadsUpView(mEntry);
-        verify(mLogger).entryContentViewMarkedFreeable(eq("key"));
+        verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         callback.get().onBindFinished(mEntry);
-        verify(mLogger).entryUnbound(eq("key"));
+        verify(mLogger).entryUnbound(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
     }
@@ -111,12 +111,12 @@
         });
 
         mViewBinder.bindHeadsUpView(mEntry, null);
-        verify(mLogger).startBindingHun(eq("key"));
+        verify(mLogger).startBindingHun(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         mViewBinder.abortBindCallback(mEntry);
-        verify(mLogger).currentOngoingBindingAborted(eq("key"));
+        verify(mLogger).currentOngoingBindingAborted(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
@@ -135,18 +135,18 @@
         });
 
         mViewBinder.bindHeadsUpView(mEntry, null);
-        verify(mLogger).startBindingHun(eq("key"));
+        verify(mLogger).startBindingHun(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         mViewBinder.unbindHeadsUpView(mEntry);
-        verify(mLogger).currentOngoingBindingAborted(eq("key"));
-        verify(mLogger).entryContentViewMarkedFreeable(eq("key"));
+        verify(mLogger).currentOngoingBindingAborted(eq(mEntry));
+        verify(mLogger).entryContentViewMarkedFreeable(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
 
         callback.get().onBindFinished(mEntry);
-        verify(mLogger).entryUnbound(eq("key"));
+        verify(mLogger).entryUnbound(eq(mEntry));
         verifyNoMoreInteractions(mLogger);
         clearInvocations(mLogger);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt
index b24b348..cafe113 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/shade/transition/ScrimShadeTransitionControllerTest.kt
@@ -5,6 +5,8 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent
 import com.android.systemui.statusbar.policy.FakeConfigurationController
@@ -13,6 +15,7 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -21,6 +24,7 @@
 
     @Mock private lateinit var scrimController: ScrimController
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
     private val configurationController = FakeConfigurationController()
 
     private lateinit var controller: ScrimShadeTransitionController
@@ -31,9 +35,14 @@
         context.ensureTestableResources()
         controller =
             ScrimShadeTransitionController(
-                configurationController, dumpManager, scrimController, context.resources
+                configurationController,
+                dumpManager,
+                scrimController,
+                context.resources,
+                statusBarStateController
             )
     }
+
     @Test
     fun onPanelExpansionChanged_inSingleShade_setsFractionEqualToEventFraction() {
         setSplitShadeEnabled(false)
@@ -44,7 +53,9 @@
     }
 
     @Test
-    fun onPanelExpansionChanged_inSplitShade_setsFractionBasedOnDragDownAmount() {
+    fun onPanelExpansionChanged_inSplitShade_unlockedShade_setsFractionBasedOnDragDownAmount() {
+        whenever(statusBarStateController.currentOrUpcomingState)
+            .thenReturn(StatusBarState.SHADE)
         val scrimShadeTransitionDistance =
             context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance)
         setSplitShadeEnabled(true)
@@ -55,6 +66,54 @@
         verify(scrimController).setRawPanelExpansionFraction(expectedFraction)
     }
 
+    @Test
+    fun onPanelExpansionChanged_inSplitShade_largeDragDownAmount_fractionIsNotGreaterThan1() {
+        whenever(statusBarStateController.currentOrUpcomingState)
+            .thenReturn(StatusBarState.SHADE)
+        val scrimShadeTransitionDistance =
+            context.resources.getDimensionPixelSize(R.dimen.split_shade_scrim_transition_distance)
+        setSplitShadeEnabled(true)
+
+        controller.onPanelExpansionChanged(
+            EXPANSION_EVENT.copy(dragDownPxAmount = 100f * scrimShadeTransitionDistance)
+        )
+
+        verify(scrimController).setRawPanelExpansionFraction(1f)
+    }
+
+    @Test
+    fun onPanelExpansionChanged_inSplitShade_negativeDragDownAmount_fractionIsNotLessThan0() {
+        whenever(statusBarStateController.currentOrUpcomingState)
+            .thenReturn(StatusBarState.SHADE)
+        setSplitShadeEnabled(true)
+
+        controller.onPanelExpansionChanged(EXPANSION_EVENT.copy(dragDownPxAmount = -100f))
+
+        verify(scrimController).setRawPanelExpansionFraction(0f)
+    }
+
+    @Test
+    fun onPanelExpansionChanged_inSplitShade_onLockedShade_setsFractionEqualToEventFraction() {
+        whenever(statusBarStateController.currentOrUpcomingState)
+            .thenReturn(StatusBarState.SHADE_LOCKED)
+        setSplitShadeEnabled(true)
+
+        controller.onPanelExpansionChanged(EXPANSION_EVENT)
+
+        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
+    }
+
+    @Test
+    fun onPanelExpansionChanged_inSplitShade_onKeyguard_setsFractionEqualToEventFraction() {
+        whenever(statusBarStateController.currentOrUpcomingState)
+            .thenReturn(StatusBarState.KEYGUARD)
+        setSplitShadeEnabled(true)
+
+        controller.onPanelExpansionChanged(EXPANSION_EVENT)
+
+        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
+    }
+
     private fun setSplitShadeEnabled(enabled: Boolean) {
         overrideResource(R.bool.config_use_split_notification_shade, enabled)
         configurationController.notifyConfigurationChanged()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index fec2123..fda80a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Intent;
+import android.os.BatteryManager;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
@@ -196,4 +197,26 @@
         TestableLooper.get(this).processAllMessages();
         // Should not throw an exception
     }
+
+    @Test
+    public void batteryStateChanged_withChargingSourceDock_isChargingSourceDockTrue() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_DOCK);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertTrue(mBatteryController.isChargingSourceDock());
+    }
+
+    @Test
+    public void batteryStateChanged_withChargingSourceNotDock_isChargingSourceDockFalse() {
+        Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        intent.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_DISCHARGING);
+        intent.putExtra(BatteryManager.EXTRA_PLUGGED, BatteryManager.BATTERY_PLUGGED_WIRELESS);
+
+        mBatteryController.onReceive(getContext(), intent);
+
+        Assert.assertFalse(mBatteryController.isChargingSourceDock());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
new file mode 100644
index 0000000..db0029a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FlashlightControllerImplTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.content.pm.PackageManager
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.impl.CameraMetadataNative
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.time.FakeSystemClock
+import java.util.concurrent.Executor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class FlashlightControllerImplTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var dumpManager: DumpManager
+
+    @Mock
+    private lateinit var cameraManager: CameraManager
+
+    @Mock
+    private lateinit var broadcastSender: BroadcastSender
+
+    @Mock
+    private lateinit var packageManager: PackageManager
+
+    private lateinit var fakeSettings: FakeSettings
+    private lateinit var fakeSystemClock: FakeSystemClock
+    private lateinit var backgroundExecutor: FakeExecutor
+    private lateinit var controller: FlashlightControllerImpl
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        fakeSystemClock = FakeSystemClock()
+        backgroundExecutor = FakeExecutor(fakeSystemClock)
+        fakeSettings = FakeSettings()
+
+        `when`(packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH))
+                .thenReturn(true)
+
+        controller = FlashlightControllerImpl(
+                dumpManager,
+                cameraManager,
+                backgroundExecutor,
+                fakeSettings,
+                broadcastSender,
+                packageManager
+        )
+    }
+
+    @Test
+    fun testNoCameraManagerInteractionDirectlyOnConstructor() {
+        verifyZeroInteractions(cameraManager)
+    }
+
+    @Test
+    fun testCameraManagerInitAfterConstructionOnExecutor() {
+        injectCamera()
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager).registerTorchCallback(eq(backgroundExecutor), any())
+    }
+
+    @Test
+    fun testNoCallbackIfNoFlashCamera() {
+        injectCamera(flash = false)
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any())
+    }
+
+    @Test
+    fun testNoCallbackIfNoBackCamera() {
+        injectCamera(facing = CameraCharacteristics.LENS_FACING_FRONT)
+        backgroundExecutor.runAllReady()
+
+        verify(cameraManager, never()).registerTorchCallback(any<Executor>(), any())
+    }
+
+    @Test
+    fun testSetFlashlightInBackgroundExecutor() {
+        val id = injectCamera()
+        backgroundExecutor.runAllReady()
+
+        clearInvocations(cameraManager)
+        val enable = !controller.isEnabled
+        controller.setFlashlight(enable)
+        verifyNoMoreInteractions(cameraManager)
+
+        backgroundExecutor.runAllReady()
+        verify(cameraManager).setTorchMode(id, enable)
+    }
+
+    private fun injectCamera(
+        flash: Boolean = true,
+        facing: Int = CameraCharacteristics.LENS_FACING_BACK
+    ): String {
+        val cameraID = "ID"
+        val camera = CameraCharacteristics(CameraMetadataNative().apply {
+            set(CameraCharacteristics.FLASH_INFO_AVAILABLE, flash)
+            set(CameraCharacteristics.LENS_FACING, facing)
+        })
+        `when`(cameraManager.cameraIdList).thenReturn(arrayOf(cameraID))
+        `when`(cameraManager.getCameraCharacteristics(cameraID)).thenReturn(camera)
+        return cameraID
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
index 424a40058..b8e25ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HeadsUpManagerTest.java
@@ -106,7 +106,7 @@
     public void testHunRemovedLogging() {
         mAlertEntry.mEntry = mEntry;
         mHeadsUpManager.onAlertEntryRemoved(mAlertEntry);
-        verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(mEntry.getKey()));
+        verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(mEntry));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
index 1e35b0f..125b362 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionMonitorTest.java
@@ -159,6 +159,24 @@
         Mockito.clearInvocations(callback);
     }
 
+    // Ensure that updating a callback that is removed doesn't result in an exception due to the
+    // absence of the condition.
+    @Test
+    public void testUpdateRemovedCallback() {
+        final Monitor.Callback callback1 =
+                mock(Monitor.Callback.class);
+        final Monitor.Subscription.Token subscription1 =
+                mConditionMonitor.addSubscription(getDefaultBuilder(callback1).build());
+        ArgumentCaptor<Condition.Callback> monitorCallback =
+                ArgumentCaptor.forClass(Condition.Callback.class);
+        mExecutor.runAllReady();
+        verify(mCondition1).addCallback(monitorCallback.capture());
+        // This will execute first before the handler for onConditionChanged.
+        mConditionMonitor.removeSubscription(subscription1);
+        monitorCallback.getValue().onConditionChanged(mCondition1);
+        mExecutor.runAllReady();
+    }
+
     @Test
     public void addCallback_addFirstCallback_addCallbackToAllConditions() {
         final Monitor.Callback callback1 =
diff --git a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
index 229799a..d5991d3 100644
--- a/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
+++ b/services/companion/java/com/android/server/companion/AssociationStoreImpl.java
@@ -73,6 +73,9 @@
     private final Set<OnChangeListener> mListeners = new LinkedHashSet<>();
 
     void addAssociation(@NonNull AssociationInfo association) {
+        // Validity check first.
+        checkNotRevoked(association);
+
         final int id = association.getId();
 
         if (DEBUG) {
@@ -99,6 +102,9 @@
     }
 
     void updateAssociation(@NonNull AssociationInfo updated) {
+        // Validity check first.
+        checkNotRevoked(updated);
+
         final int id = updated.getId();
 
         if (DEBUG) {
@@ -292,6 +298,9 @@
     }
 
     void setAssociations(Collection<AssociationInfo> allAssociations) {
+        // Validity check first.
+        allAssociations.forEach(AssociationStoreImpl::checkNotRevoked);
+
         if (DEBUG) {
             Log.i(TAG, "setAssociations() n=" + allAssociations.size());
             final StringJoiner stringJoiner = new StringJoiner(", ");
@@ -324,4 +333,11 @@
         mAddressMap.clear();
         mCachedPerUser.clear();
     }
+
+    private static void checkNotRevoked(@NonNull AssociationInfo association) {
+        if (association.isRevoked()) {
+            throw new IllegalArgumentException(
+                    "Revoked (removed) associations MUST NOT appear in the AssociationStore");
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 3f7cba6..abc4937 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -18,6 +18,7 @@
 package com.android.server.companion;
 
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -48,6 +49,8 @@
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
@@ -91,6 +94,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IAppOpsService;
 import com.android.internal.content.PackageMonitor;
+import com.android.internal.infra.PerUser;
 import com.android.internal.notification.NotificationAccessConfirmationActivityContract;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.ArrayUtils;
@@ -100,10 +104,12 @@
 import com.android.server.SystemService;
 import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
 import com.android.server.pm.UserManagerInternal;
+import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -127,6 +133,9 @@
 
     private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
 
+    private final ActivityManager mActivityManager;
+    private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
+
     private PersistentDataStore mPersistentStore;
     private final PersistUserStateHandler mUserPersistenceHandler;
 
@@ -135,6 +144,7 @@
     private CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private CompanionApplicationController mCompanionAppController;
 
+    private final ActivityTaskManagerInternal mAtmInternal;
     private final ActivityManagerInternal mAmInternal;
     private final IAppOpsService mAppOpsManager;
     private final PowerWhitelistManager mPowerWhitelistManager;
@@ -150,21 +160,53 @@
     @GuardedBy("mPreviouslyUsedIds")
     private final SparseArray<Map<String, Set<Integer>>> mPreviouslyUsedIds = new SparseArray<>();
 
+    /**
+     * A structure that consists of a set of revoked associations that pending for role holder
+     * removal per each user.
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
+     */
+    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
+    private final PerUserAssociationSet mRevokedAssociationsPendingRoleHolderRemoval =
+            new PerUserAssociationSet();
+    /**
+     * Contains uid-s of packages pending to be removed from the role holder list (after
+     * revocation of an association), which will happen one the package is no longer visible to the
+     * user.
+     * For quicker uid -> (userId, packageName) look-up this is not a {@code Set<Integer>} but
+     * a {@code Map<Integer, String>} which maps uid-s to packageName-s (userId-s can be derived
+     * from uid-s using {@link UserHandle#getUserId(int)}).
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #addToPendingRoleHolderRemoval(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     */
+    @GuardedBy("mRevokedAssociationsPendingRoleHolderRemoval")
+    private final Map<Integer, String> mUidsPendingRoleHolderRemoval = new HashMap<>();
+
     private final RemoteCallbackList<IOnAssociationsChangedListener> mListeners =
             new RemoteCallbackList<>();
 
     public CompanionDeviceManagerService(Context context) {
         super(context);
 
+        mActivityManager = context.getSystemService(ActivityManager.class);
         mPowerWhitelistManager = context.getSystemService(PowerWhitelistManager.class);
         mAppOpsManager = IAppOpsService.Stub.asInterface(
                 ServiceManager.getService(Context.APP_OPS_SERVICE));
+        mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
         mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         mUserManager = context.getSystemService(UserManager.class);
 
         mUserPersistenceHandler = new PersistUserStateHandler();
         mAssociationStore = new AssociationStoreImpl();
+
+        mOnPackageVisibilityChangeListener =
+                new OnPackageVisibilityChangeListener(mActivityManager);
     }
 
     @Override
@@ -201,7 +243,33 @@
                     mUserManager.getAliveUsers(), allAssociations, mPreviouslyUsedIds);
         }
 
-        mAssociationStore.setAssociations(allAssociations);
+        final Set<AssociationInfo> activeAssociations =
+                new ArraySet<>(/* capacity */ allAssociations.size());
+        // A set contains the userIds that need to persist state after remove the app
+        // from the list of role holders.
+        final Set<Integer> usersToPersistStateFor = new ArraySet<>();
+
+        for (AssociationInfo association : allAssociations) {
+            if (!association.isRevoked()) {
+                activeAssociations.add(association);
+            } else if (maybeRemoveRoleHolderForAssociation(association)) {
+                // Nothing more to do here, but we'll need to persist all the associations to the
+                // disk afterwards.
+                usersToPersistStateFor.add(association.getUserId());
+            } else {
+                addToPendingRoleHolderRemoval(association);
+            }
+        }
+
+        mAssociationStore.setAssociations(activeAssociations);
+
+        // IMPORTANT: only do this AFTER mAssociationStore.setAssociations(), because
+        // persistStateForUser() queries AssociationStore.
+        // (If persistStateForUser() is invoked before mAssociationStore.setAssociations() it
+        // would effectively just clear-out all the persisted associations).
+        for (int userId : usersToPersistStateFor) {
+            persistStateForUser(userId);
+        }
     }
 
     @Override
@@ -351,10 +419,18 @@
     }
 
     private void persistStateForUser(@UserIdInt int userId) {
-        final List<AssociationInfo> updatedAssociations =
-                mAssociationStore.getAssociationsForUser(userId);
+        // We want to store both active associations and the revoked (removed) association that we
+        // are keeping around for the final clean-up (delayed role holder removal).
+        final List<AssociationInfo> allAssociations;
+        // Start with the active associations - these we can get from the AssociationStore.
+        allAssociations = new ArrayList<>(
+                mAssociationStore.getAssociationsForUser(userId));
+        // ... and add the revoked (removed) association, that are yet to be permanently removed.
+        allAssociations.addAll(getPendingRoleHolderRemovalAssociationsForUser(userId));
+
         final Map<String, Set<Integer>> usedIdsForUser = getPreviouslyUsedIdsForUser(userId);
-        mPersistentStore.persistStateForUser(userId, updatedAssociations, usedIdsForUser);
+
+        mPersistentStore.persistStateForUser(userId, allAssociations, usedIdsForUser);
     }
 
     private void notifyListeners(
@@ -422,13 +498,17 @@
             removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
         }
 
-        for (AssociationInfo ai : mAssociationStore.getAssociations()) {
-            if (!ai.isSelfManaged()) continue;
-            final boolean isInactive =  currentTime - ai.getLastTimeConnectedMs() >= removalWindow;
-            if (isInactive) {
-                Slog.i(TAG, "Removing inactive self-managed association: " + ai.getId());
-                disassociateInternal(ai.getId());
-            }
+        for (AssociationInfo association : mAssociationStore.getAssociations()) {
+            if (!association.isSelfManaged()) continue;
+
+            final boolean isInactive =
+                    currentTime - association.getLastTimeConnectedMs() >= removalWindow;
+            if (!isInactive) continue;
+
+            final int id = association.getId();
+
+            Slog.i(TAG, "Removing inactive self-managed association id=" + id);
+            disassociateInternal(id);
         }
     }
 
@@ -668,7 +748,7 @@
             enforceCallerIsSystemOr(userId, packageName);
 
             AssociationInfo association = mAssociationStore.getAssociationsForPackageWithAddress(
-                            userId, packageName, deviceAddress);
+                    userId, packageName, deviceAddress);
 
             if (association == null) {
                 throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
@@ -728,7 +808,7 @@
 
             enforceUsesCompanionDeviceFeature(getContext(), userId, callingPackage);
             checkState(!ArrayUtils.isEmpty(
-                    mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
+                            mAssociationStore.getAssociationsForPackage(userId, callingPackage)),
                     "App must have an association before calling this API");
         }
 
@@ -788,8 +868,8 @@
         final long timestamp = System.currentTimeMillis();
 
         final AssociationInfo association = new AssociationInfo(id, userId, packageName,
-                macAddress, displayName, deviceProfile, selfManaged, false, timestamp,
-                Long.MAX_VALUE);
+                macAddress, displayName, deviceProfile, selfManaged,
+                /* notifyOnDeviceNearby */ false, /* revoked */ false, timestamp, Long.MAX_VALUE);
         Slog.i(TAG, "New CDM association created=" + association);
         mAssociationStore.addAssociation(association);
 
@@ -801,6 +881,11 @@
 
         updateSpecialAccessPermissionForAssociatedPackage(association);
         logCreateAssociation(deviceProfile);
+
+        // Don't need to update the mRevokedAssociationsPendingRoleHolderRemoval since
+        // maybeRemoveRoleHolderForAssociation in PackageInactivityListener will handle the case
+        // that there are other devices with the same profile, so the role holder won't be removed.
+
         return association;
     }
 
@@ -881,36 +966,184 @@
         final String packageName = association.getPackageName();
         final String deviceProfile = association.getDeviceProfile();
 
+        if (!maybeRemoveRoleHolderForAssociation(association)) {
+            // Need to remove the app from list of the role holders, but will have to do it later
+            // (the app is in foreground at the moment).
+            addToPendingRoleHolderRemoval(association);
+        }
+
+        // Need to check if device still present now because CompanionDevicePresenceMonitor will
+        // remove current connected device after mAssociationStore.removeAssociation
         final boolean wasPresent = mDevicePresenceMonitor.isDevicePresent(associationId);
 
         // Removing the association.
         mAssociationStore.removeAssociation(associationId);
+        // Do not need to persistUserState since CompanionDeviceManagerService will get callback
+        // from #onAssociationChanged, and it will handle the persistUserState which including
+        // active and revoked association.
         logRemoveAssociation(deviceProfile);
 
-        final List<AssociationInfo> otherAssociations =
-                mAssociationStore.getAssociationsForPackage(userId, packageName);
-
-        // Check if the package is associated with other devices with the same profile.
-        // If not: take away the role.
-        if (deviceProfile != null) {
-            final boolean shouldKeepTheRole = any(otherAssociations,
-                    it -> deviceProfile.equals(it.getDeviceProfile()));
-            if (!shouldKeepTheRole) {
-                Binder.withCleanCallingIdentity(() ->
-                        removeRoleHolderForAssociation(getContext(), association));
-            }
-        }
-
         if (!wasPresent || !association.isNotifyOnDeviceNearby()) return;
         // The device was connected and the app was notified: check if we need to unbind the app
         // now.
-        final boolean shouldStayBound = any(otherAssociations,
+        final boolean shouldStayBound = any(
+                mAssociationStore.getAssociationsForPackage(userId, packageName),
                 it -> it.isNotifyOnDeviceNearby()
                         && mDevicePresenceMonitor.isDevicePresent(it.getId()));
         if (shouldStayBound) return;
         mCompanionAppController.unbindCompanionApplication(userId, packageName);
     }
 
+    /**
+     * First, checks if the companion application should be removed from the list role holders when
+     * upon association's removal, i.e.: association's profile (matches the role) is not null,
+     * the application does not have other associations with the same profile, etc.
+     *
+     * <p>
+     * Then, if establishes that the application indeed has to be removed from the list of the role
+     * holders, checks if it could be done right now -
+     * {@link android.app.role.RoleManager#removeRoleHolderAsUser(String, String, int, UserHandle, java.util.concurrent.Executor, java.util.function.Consumer) RoleManager#removeRoleHolderAsUser()}
+     * will kill the application's process, which leads poor user experience if the application was
+     * in foreground when this happened, to avoid this CDMS delays invoking
+     * {@code RoleManager.removeRoleHolderAsUser()} until the app is no longer in foreground.
+     *
+     * @return {@code true} if the application does NOT need be removed from the list of the role
+     *         holders OR if the application was successfully removed from the list of role holders.
+     *         I.e.: from the role-management perspective the association is done with.
+     *         {@code false} if the application needs to be removed from the list of role the role
+     *         holders, BUT it CDMS would prefer to do it later.
+     *         I.e.: application is in the foreground at the moment, but invoking
+     *         {@code RoleManager.removeRoleHolderAsUser()} will kill the application's process,
+     *         which would lead to the poor UX, hence need to try later.
+     */
+
+    private boolean maybeRemoveRoleHolderForAssociation(@NonNull AssociationInfo association) {
+        if (DEBUG) Log.d(TAG, "maybeRemoveRoleHolderForAssociation() association=" + association);
+
+        final String deviceProfile = association.getDeviceProfile();
+        if (deviceProfile == null) {
+            // No role was granted to for this association, there is nothing else we need to here.
+            return true;
+        }
+
+        // Check if the applications is associated with another devices with the profile. If so,
+        // it should remain the role holder.
+        final int id = association.getId();
+        final int userId = association.getUserId();
+        final String packageName = association.getPackageName();
+        final boolean roleStillInUse = any(
+                mAssociationStore.getAssociationsForPackage(userId, packageName),
+                it -> deviceProfile.equals(it.getDeviceProfile()) && id != it.getId());
+        if (roleStillInUse) {
+            // Application should remain a role holder, there is nothing else we need to here.
+            return true;
+        }
+
+        final int packageProcessImportance = getPackageProcessImportance(userId, packageName);
+        if (packageProcessImportance <= IMPORTANCE_VISIBLE) {
+            // Need to remove the app from the list of role holders, but the process is visible to
+            // the user at the moment, so we'll need to it later: log and return false.
+            Slog.i(TAG, "Cannot remove role holder for the removed association id=" + id
+                    + " now - process is visible.");
+            return false;
+        }
+
+        removeRoleHolderForAssociation(getContext(), association);
+        return true;
+    }
+
+    private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
+        return Binder.withCleanCallingIdentity(() -> {
+            final int uid =
+                    mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+            return mActivityManager.getUidImportance(uid);
+        });
+    }
+
+    /**
+     * Set revoked flag for active association and add the revoked association and the uid into
+     * the caches.
+     *
+     * @see #mRevokedAssociationsPendingRoleHolderRemoval
+     * @see #mUidsPendingRoleHolderRemoval
+     * @see OnPackageVisibilityChangeListener
+     */
+    private void addToPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
+        // First: set revoked flag.
+        association = AssociationInfo.builder(association)
+                .setRevoked(true)
+                .build();
+
+        final String packageName = association.getPackageName();
+        final int userId = association.getUserId();
+        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+
+        // Second: add to the set.
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            mRevokedAssociationsPendingRoleHolderRemoval.forUser(association.getUserId())
+                    .add(association);
+            if (!mUidsPendingRoleHolderRemoval.containsKey(uid)) {
+                mUidsPendingRoleHolderRemoval.put(uid, packageName);
+
+                if (mUidsPendingRoleHolderRemoval.size() == 1) {
+                    // Just added first uid: start the listener
+                    mOnPackageVisibilityChangeListener.startListening();
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove the revoked association form the cache and also remove the uid form the map if
+     * there are other associations with the same package still pending for role holder removal.
+     *
+     * @see #mRevokedAssociationsPendingRoleHolderRemoval
+     * @see #mUidsPendingRoleHolderRemoval
+     * @see OnPackageVisibilityChangeListener
+     */
+    private void removeFromPendingRoleHolderRemoval(@NonNull AssociationInfo association) {
+        final String packageName = association.getPackageName();
+        final int userId = association.getUserId();
+        final int uid = mPackageManagerInternal.getPackageUid(packageName, /* flags */0, userId);
+
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId)
+                    .remove(association);
+
+            final boolean shouldKeepUidForRemoval = any(
+                    getPendingRoleHolderRemovalAssociationsForUser(userId),
+                    ai -> packageName.equals(ai.getPackageName()));
+            // Do not remove the uid form the map since other associations with
+            // the same packageName still pending for role holder removal.
+            if (!shouldKeepUidForRemoval) {
+                mUidsPendingRoleHolderRemoval.remove(uid);
+            }
+
+            if (mUidsPendingRoleHolderRemoval.isEmpty()) {
+                // The set is empty now - can "turn off" the listener.
+                mOnPackageVisibilityChangeListener.stopListening();
+            }
+        }
+    }
+
+    /**
+     * @return a copy of the revoked associations set (safeguarding against
+     *         {@code ConcurrentModificationException}-s).
+     */
+    private @NonNull Set<AssociationInfo> getPendingRoleHolderRemovalAssociationsForUser(
+            @UserIdInt int userId) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            // Return a copy.
+            return new ArraySet<>(mRevokedAssociationsPendingRoleHolderRemoval.forUser(userId));
+        }
+    }
+
+    private String getPackageNameByUid(int uid) {
+        synchronized (mRevokedAssociationsPendingRoleHolderRemoval) {
+            return mUidsPendingRoleHolderRemoval.get(uid);
+        }
+    }
+
     private void updateSpecialAccessPermissionForAssociatedPackage(AssociationInfo association) {
         final PackageInfo packageInfo =
                 getPackageInfo(getContext(), association.getUserId(), association.getPackageName());
@@ -969,6 +1202,9 @@
                 companionAppUids.add(uid);
             }
         }
+        if (mAtmInternal != null) {
+            mAtmInternal.setCompanionAppUids(userId, companionAppUids);
+        }
         if (mAmInternal != null) {
             // Make a copy of the set and send it to ActivityManager.
             mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids));
@@ -1128,4 +1364,80 @@
             persistStateForUser(userId);
         }
     }
+
+    /**
+     * An OnUidImportanceListener class which watches the importance of the packages.
+     * In this class, we ONLY interested in the importance of the running process is greater than
+     * {@link RunningAppProcessInfo.IMPORTANCE_VISIBLE} for the uids have been added into the
+     * {@link mUidsPendingRoleHolderRemoval}. Lastly remove the role holder for the revoked
+     * associations for the same packages.
+     *
+     * @see #maybeRemoveRoleHolderForAssociation(AssociationInfo)
+     * @see #removeFromPendingRoleHolderRemoval(AssociationInfo)
+     * @see #getPendingRoleHolderRemovalAssociationsForUser(int)
+     */
+    private class OnPackageVisibilityChangeListener implements
+            ActivityManager.OnUidImportanceListener {
+        final @NonNull ActivityManager mAm;
+
+        OnPackageVisibilityChangeListener(@NonNull ActivityManager am) {
+            this.mAm = am;
+        }
+
+        void startListening() {
+            Binder.withCleanCallingIdentity(
+                    () -> mAm.addOnUidImportanceListener(
+                            /* listener */ OnPackageVisibilityChangeListener.this,
+                            RunningAppProcessInfo.IMPORTANCE_VISIBLE));
+        }
+
+        void stopListening() {
+            Binder.withCleanCallingIdentity(
+                    () -> mAm.removeOnUidImportanceListener(
+                            /* listener */ OnPackageVisibilityChangeListener.this));
+        }
+
+        @Override
+        public void onUidImportance(int uid, int importance) {
+            if (importance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
+                // The lower the importance value the more "important" the process is.
+                // We are only interested when the process ceases to be visible.
+                return;
+            }
+
+            final String packageName = getPackageNameByUid(uid);
+            if (packageName == null) {
+                // Not interested in this uid.
+                return;
+            }
+
+            final int userId = UserHandle.getUserId(uid);
+
+            boolean needToPersistStateForUser = false;
+
+            for (AssociationInfo association :
+                    getPendingRoleHolderRemovalAssociationsForUser(userId)) {
+                if (!packageName.equals(association.getPackageName())) continue;
+
+                if (!maybeRemoveRoleHolderForAssociation(association)) {
+                    // Did not remove the role holder, will have to try again later.
+                    continue;
+                }
+
+                removeFromPendingRoleHolderRemoval(association);
+                needToPersistStateForUser = true;
+            }
+
+            if (needToPersistStateForUser) {
+                mUserPersistenceHandler.postPersistUserState(userId);
+            }
+        }
+    }
+
+    private static class PerUserAssociationSet extends PerUser<Set<AssociationInfo>> {
+        @Override
+        protected @NonNull Set<AssociationInfo> create(int userId) {
+            return new ArraySet<>();
+        }
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/PersistentDataStore.java b/services/companion/java/com/android/server/companion/PersistentDataStore.java
index 3639389..4b56c1b 100644
--- a/services/companion/java/com/android/server/companion/PersistentDataStore.java
+++ b/services/companion/java/com/android/server/companion/PersistentDataStore.java
@@ -103,7 +103,7 @@
  * Since Android T the data is stored to "companion_device_manager.xml" file in
  * {@link Environment#getDataSystemDeDirectory(int) /data/system_de/}.
  *
- * See {@link #getBaseStorageFileForUser(int) getBaseStorageFileForUser()}
+ * See {@link DataStoreUtils#getBaseStorageFileForUser(int, String)}
  *
  * <p>
  * Since Android T the data is stored using the v1 schema.
@@ -120,7 +120,7 @@
  * <li> {@link #readPreviouslyUsedIdsV1(TypedXmlPullParser, Map) readPreviouslyUsedIdsV1()}
  * </ul>
  *
- * The following snippet is a sample of a file that is using v0 schema.
+ * The following snippet is a sample of a file that is using v1 schema.
  * <pre>{@code
  * <state persistence-version="1">
  *     <associations>
@@ -130,6 +130,8 @@
  *             mac_address="AA:BB:CC:DD:EE:00"
  *             self_managed="false"
  *             notify_device_nearby="false"
+ *             revoked="false"
+ *             last_time_connected="1634641160229"
  *             time_approved="1634389553216"/>
  *
  *         <association
@@ -139,6 +141,8 @@
  *             display_name="Jhon's Chromebook"
  *             self_managed="true"
  *             notify_device_nearby="false"
+ *             revoked="false"
+ *             last_time_connected="1634641160229"
  *             time_approved="1634641160229"/>
  *     </associations>
  *
@@ -178,6 +182,7 @@
     private static final String XML_ATTR_PROFILE = "profile";
     private static final String XML_ATTR_SELF_MANAGED = "self_managed";
     private static final String XML_ATTR_NOTIFY_DEVICE_NEARBY = "notify_device_nearby";
+    private static final String XML_ATTR_REVOKED = "revoked";
     private static final String XML_ATTR_TIME_APPROVED = "time_approved";
     private static final String XML_ATTR_LAST_TIME_CONNECTED = "last_time_connected";
 
@@ -415,7 +420,8 @@
 
         out.add(new AssociationInfo(associationId, userId, appPackage,
                 MacAddress.fromString(deviceAddress), null, profile,
-                /* managedByCompanionApp */false, notify, timeApproved, Long.MAX_VALUE));
+                /* managedByCompanionApp */ false, notify, /* revoked */ false, timeApproved,
+                Long.MAX_VALUE));
     }
 
     private static void readAssociationsV1(@NonNull TypedXmlPullParser parser,
@@ -444,13 +450,14 @@
         final String displayName = readStringAttribute(parser, XML_ATTR_DISPLAY_NAME);
         final boolean selfManaged = readBooleanAttribute(parser, XML_ATTR_SELF_MANAGED);
         final boolean notify = readBooleanAttribute(parser, XML_ATTR_NOTIFY_DEVICE_NEARBY);
+        final boolean revoked = readBooleanAttribute(parser, XML_ATTR_REVOKED, false);
         final long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED, 0L);
         final long lastTimeConnected = readLongAttribute(
                 parser, XML_ATTR_LAST_TIME_CONNECTED, Long.MAX_VALUE);
 
         final AssociationInfo associationInfo = createAssociationInfoNoThrow(associationId, userId,
-                appPackage, macAddress, displayName, profile, selfManaged, notify, timeApproved,
-                lastTimeConnected);
+                appPackage, macAddress, displayName, profile, selfManaged, notify, revoked,
+                timeApproved, lastTimeConnected);
         if (associationInfo != null) {
             out.add(associationInfo);
         }
@@ -503,6 +510,8 @@
         writeBooleanAttribute(serializer, XML_ATTR_SELF_MANAGED, a.isSelfManaged());
         writeBooleanAttribute(
                 serializer, XML_ATTR_NOTIFY_DEVICE_NEARBY, a.isNotifyOnDeviceNearby());
+        writeBooleanAttribute(
+                serializer, XML_ATTR_REVOKED, a.isRevoked());
         writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, a.getTimeApprovedMs());
         writeLongAttribute(
                 serializer, XML_ATTR_LAST_TIME_CONNECTED, a.getLastTimeConnectedMs());
@@ -544,11 +553,12 @@
     private static AssociationInfo createAssociationInfoNoThrow(int associationId,
             @UserIdInt int userId, @NonNull String appPackage, @Nullable MacAddress macAddress,
             @Nullable CharSequence displayName, @Nullable String profile, boolean selfManaged,
-            boolean notify, long timeApproved, long lastTimeConnected) {
+            boolean notify, boolean revoked, long timeApproved, long lastTimeConnected) {
         AssociationInfo associationInfo = null;
         try {
             associationInfo = new AssociationInfo(associationId, userId, appPackage, macAddress,
-                    displayName, profile, selfManaged, notify, timeApproved, lastTimeConnected);
+                    displayName, profile, selfManaged, notify, revoked, timeApproved,
+                    lastTimeConnected);
         } catch (Exception e) {
             if (DEBUG) Log.w(TAG, "Could not create AssociationInfo", e);
         }
diff --git a/services/companion/java/com/android/server/companion/RolesUtils.java b/services/companion/java/com/android/server/companion/RolesUtils.java
index 35488a8..0fff3f4 100644
--- a/services/companion/java/com/android/server/companion/RolesUtils.java
+++ b/services/companion/java/com/android/server/companion/RolesUtils.java
@@ -85,6 +85,8 @@
         final int userId = associationInfo.getUserId();
         final UserHandle userHandle = UserHandle.of(userId);
 
+        Slog.i(TAG, "Removing CDM role holder, role=" + deviceProfile
+                + ", package=u" + userId + "\\" + packageName);
         roleManager.removeRoleHolderAsUser(deviceProfile, packageName,
                 MANAGE_HOLDERS_FLAG_DONT_KILL_APP, userHandle, context.getMainExecutor(),
                 success -> {
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 297d28d..56990ed 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -19,7 +19,7 @@
 import static com.android.internal.util.dump.DumpUtils.writeStringIfNotNull;
 
 import android.annotation.NonNull;
-import android.annotation.TestApi;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -102,11 +102,26 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
- * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keysi
+ * Provides communication to the Android Debug Bridge daemon to allow, deny, or clear public keys
  * that are authorized to connect to the ADB service itself.
+ *
+ * <p>The AdbDebuggingManager controls two files:
+ * <ol>
+ *     <li>adb_keys
+ *     <li>adb_temp_keys.xml
+ * </ol>
+ *
+ * <p>The ADB Daemon (adbd) reads <em>only</em> the adb_keys file for authorization. Public keys
+ * from registered hosts are stored in adb_keys, one entry per line.
+ *
+ * <p>AdbDebuggingManager also keeps adb_temp_keys.xml, which is used for two things
+ * <ol>
+ *     <li>Removing unused keys from the adb_keys file
+ *     <li>Managing authorized WiFi access points for ADB over WiFi
+ * </ol>
  */
 public class AdbDebuggingManager {
-    private static final String TAG = "AdbDebuggingManager";
+    private static final String TAG = AdbDebuggingManager.class.getSimpleName();
     private static final boolean DEBUG = false;
     private static final boolean MDNS_DEBUG = false;
 
@@ -118,18 +133,20 @@
     // as a subsequent connection occurs within the allowed duration.
     private static final String ADB_TEMP_KEYS_FILE = "adb_temp_keys.xml";
     private static final int BUFFER_SIZE = 65536;
+    private static final Ticker SYSTEM_TICKER = () -> System.currentTimeMillis();
 
     private final Context mContext;
     private final ContentResolver mContentResolver;
-    private final Handler mHandler;
-    private AdbDebuggingThread mThread;
+    @VisibleForTesting final AdbDebuggingHandler mHandler;
+    @Nullable private AdbDebuggingThread mThread;
     private boolean mAdbUsbEnabled = false;
     private boolean mAdbWifiEnabled = false;
     private String mFingerprints;
     // A key can be used more than once (e.g. USB, wifi), so need to keep a refcount
-    private final Map<String, Integer> mConnectedKeys;
-    private String mConfirmComponent;
-    private final File mTestUserKeyFile;
+    private final Map<String, Integer> mConnectedKeys = new HashMap<>();
+    private final String mConfirmComponent;
+    @Nullable private final File mUserKeyFile;
+    @Nullable private final File mTempKeysFile;
 
     private static final String WIFI_PERSISTENT_CONFIG_PROPERTY =
             "persist.adb.tls_server.enable";
@@ -138,37 +155,44 @@
     private static final int PAIRING_CODE_LENGTH = 6;
     private PairingThread mPairingThread = null;
     // A list of keys connected via wifi
-    private final Set<String> mWifiConnectedKeys;
+    private final Set<String> mWifiConnectedKeys = new HashSet<>();
     // The current info of the adbwifi connection.
-    private AdbConnectionInfo mAdbConnectionInfo;
+    private AdbConnectionInfo mAdbConnectionInfo = new AdbConnectionInfo();
     // Polls for a tls port property when adb wifi is enabled
     private AdbConnectionPortPoller mConnectionPortPoller;
     private final PortListenerImpl mPortListener = new PortListenerImpl();
+    private final Ticker mTicker;
 
     public AdbDebuggingManager(Context context) {
-        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
-        mContext = context;
-        mContentResolver = mContext.getContentResolver();
-        mTestUserKeyFile = null;
-        mConnectedKeys = new HashMap<String, Integer>();
-        mWifiConnectedKeys = new HashSet<String>();
-        mAdbConnectionInfo = new AdbConnectionInfo();
+        this(
+                context,
+                /* confirmComponent= */ null,
+                getAdbFile(ADB_KEYS_FILE),
+                getAdbFile(ADB_TEMP_KEYS_FILE),
+                /* adbDebuggingThread= */ null,
+                SYSTEM_TICKER);
     }
 
     /**
      * Constructor that accepts the component to be invoked to confirm if the user wants to allow
      * an adb connection from the key.
      */
-    @TestApi
-    protected AdbDebuggingManager(Context context, String confirmComponent, File testUserKeyFile) {
-        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper());
+    @VisibleForTesting
+    AdbDebuggingManager(
+            Context context,
+            String confirmComponent,
+            File testUserKeyFile,
+            File tempKeysFile,
+            AdbDebuggingThread adbDebuggingThread,
+            Ticker ticker) {
         mContext = context;
         mContentResolver = mContext.getContentResolver();
         mConfirmComponent = confirmComponent;
-        mTestUserKeyFile = testUserKeyFile;
-        mConnectedKeys = new HashMap<String, Integer>();
-        mWifiConnectedKeys = new HashSet<String>();
-        mAdbConnectionInfo = new AdbConnectionInfo();
+        mUserKeyFile = testUserKeyFile;
+        mTempKeysFile = tempKeysFile;
+        mThread = adbDebuggingThread;
+        mTicker = ticker;
+        mHandler = new AdbDebuggingHandler(FgThread.get().getLooper(), mThread);
     }
 
     static void sendBroadcastWithDebugPermission(@NonNull Context context, @NonNull Intent intent,
@@ -189,8 +213,7 @@
         // consisting of only letters, digits, and hyphens, must begin and end
         // with a letter or digit, must not contain consecutive hyphens, and
         // must contain at least one letter.
-        @VisibleForTesting
-        static final String SERVICE_PROTOCOL = "adb-tls-pairing";
+        @VisibleForTesting static final String SERVICE_PROTOCOL = "adb-tls-pairing";
         private final String mServiceType = String.format("_%s._tcp.", SERVICE_PROTOCOL);
         private int mPort;
 
@@ -352,16 +375,24 @@
         }
     }
 
-    class AdbDebuggingThread extends Thread {
+    @VisibleForTesting
+    static class AdbDebuggingThread extends Thread {
         private boolean mStopped;
         private LocalSocket mSocket;
         private OutputStream mOutputStream;
         private InputStream mInputStream;
+        private Handler mHandler;
 
+        @VisibleForTesting
         AdbDebuggingThread() {
             super(TAG);
         }
 
+        @VisibleForTesting
+        void setHandler(Handler handler) {
+            mHandler = handler;
+        }
+
         @Override
         public void run() {
             if (DEBUG) Slog.d(TAG, "Entering thread");
@@ -536,7 +567,7 @@
         }
     }
 
-    class AdbConnectionInfo {
+    private static class AdbConnectionInfo {
         private String mBssid;
         private String mSsid;
         private int mPort;
@@ -743,11 +774,14 @@
         // Notification when adbd socket is disconnected.
         static final int MSG_ADBD_SOCKET_DISCONNECTED = 27;
 
+        // === Messages from other parts of the system
+        private static final int MESSAGE_KEY_FILES_UPDATED = 28;
+
         // === Messages we can send to adbd ===========
         static final String MSG_DISCONNECT_DEVICE = "DD";
         static final String MSG_DISABLE_ADBDWIFI = "DA";
 
-        private AdbKeyStore mAdbKeyStore;
+        @Nullable @VisibleForTesting AdbKeyStore mAdbKeyStore;
 
         // Usb, Wi-Fi transports can be enabled together or separately, so don't break the framework
         // connection unless all transport types are disconnected.
@@ -762,19 +796,19 @@
             }
         };
 
-        AdbDebuggingHandler(Looper looper) {
-            super(looper);
-        }
-
-        /**
-         * Constructor that accepts the AdbDebuggingThread to which responses should be sent
-         * and the AdbKeyStore to be used to store the temporary grants.
-         */
-        @TestApi
-        AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread, AdbKeyStore adbKeyStore) {
+        /** Constructor that accepts the AdbDebuggingThread to which responses should be sent. */
+        @VisibleForTesting
+        AdbDebuggingHandler(Looper looper, AdbDebuggingThread thread) {
             super(looper);
             mThread = thread;
-            mAdbKeyStore = adbKeyStore;
+        }
+
+        /** Initialize the AdbKeyStore so tests can grab mAdbKeyStore immediately. */
+        @VisibleForTesting
+        void initKeyStore() {
+            if (mAdbKeyStore == null) {
+                mAdbKeyStore = new AdbKeyStore();
+            }
         }
 
         // Show when at least one device is connected.
@@ -805,6 +839,7 @@
 
             registerForAuthTimeChanges();
             mThread = new AdbDebuggingThread();
+            mThread.setHandler(mHandler);
             mThread.start();
 
             mAdbKeyStore.updateKeyStore();
@@ -825,8 +860,7 @@
 
             if (!mConnectedKeys.isEmpty()) {
                 for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) {
-                    mAdbKeyStore.setLastConnectionTime(entry.getKey(),
-                            System.currentTimeMillis());
+                    mAdbKeyStore.setLastConnectionTime(entry.getKey(), mTicker.currentTimeMillis());
                 }
                 sendPersistKeyStoreMessage();
                 mConnectedKeys.clear();
@@ -836,9 +870,7 @@
         }
 
         public void handleMessage(Message msg) {
-            if (mAdbKeyStore == null) {
-                mAdbKeyStore = new AdbKeyStore();
-            }
+            initKeyStore();
 
             switch (msg.what) {
                 case MESSAGE_ADB_ENABLED:
@@ -873,7 +905,7 @@
                             if (!mConnectedKeys.containsKey(key)) {
                                 mConnectedKeys.put(key, 1);
                             }
-                            mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                            mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis());
                             sendPersistKeyStoreMessage();
                             scheduleJobToUpdateAdbKeyStore();
                         }
@@ -920,9 +952,7 @@
                     mConnectedKeys.clear();
                     // If the key store has not yet been instantiated then do so now; this avoids
                     // the unnecessary creation of the key store when adb is not enabled.
-                    if (mAdbKeyStore == null) {
-                        mAdbKeyStore = new AdbKeyStore();
-                    }
+                    initKeyStore();
                     mWifiConnectedKeys.clear();
                     mAdbKeyStore.deleteKeyStore();
                     cancelJobToUpdateAdbKeyStore();
@@ -937,7 +967,8 @@
                             alwaysAllow = true;
                             int refcount = mConnectedKeys.get(key) - 1;
                             if (refcount == 0) {
-                                mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                                mAdbKeyStore.setLastConnectionTime(
+                                        key, mTicker.currentTimeMillis());
                                 sendPersistKeyStoreMessage();
                                 scheduleJobToUpdateAdbKeyStore();
                                 mConnectedKeys.remove(key);
@@ -963,7 +994,7 @@
                     if (!mConnectedKeys.isEmpty()) {
                         for (Map.Entry<String, Integer> entry : mConnectedKeys.entrySet()) {
                             mAdbKeyStore.setLastConnectionTime(entry.getKey(),
-                                    System.currentTimeMillis());
+                                    mTicker.currentTimeMillis());
                         }
                         sendPersistKeyStoreMessage();
                         scheduleJobToUpdateAdbKeyStore();
@@ -984,7 +1015,7 @@
                         } else {
                             mConnectedKeys.put(key, mConnectedKeys.get(key) + 1);
                         }
-                        mAdbKeyStore.setLastConnectionTime(key, System.currentTimeMillis());
+                        mAdbKeyStore.setLastConnectionTime(key, mTicker.currentTimeMillis());
                         sendPersistKeyStoreMessage();
                         scheduleJobToUpdateAdbKeyStore();
                         logAdbConnectionChanged(key, AdbProtoEnums.AUTOMATICALLY_ALLOWED, true);
@@ -1206,6 +1237,10 @@
                     }
                     break;
                 }
+                case MESSAGE_KEY_FILES_UPDATED: {
+                    mAdbKeyStore.reloadKeyMap();
+                    break;
+                }
             }
         }
 
@@ -1377,8 +1412,7 @@
                 AdbDebuggingManager.sendBroadcastWithDebugPermission(mContext, intent,
                         UserHandle.ALL);
                 // Add the key into the keystore
-                mAdbKeyStore.setLastConnectionTime(publicKey,
-                        System.currentTimeMillis());
+                mAdbKeyStore.setLastConnectionTime(publicKey, mTicker.currentTimeMillis());
                 sendPersistKeyStoreMessage();
                 scheduleJobToUpdateAdbKeyStore();
             }
@@ -1449,19 +1483,13 @@
         extras.add(new AbstractMap.SimpleEntry<String, String>("ssid", ssid));
         extras.add(new AbstractMap.SimpleEntry<String, String>("bssid", bssid));
         int currentUserId = ActivityManager.getCurrentUser();
-        UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId);
-        String componentString;
-        if (userInfo.isAdmin()) {
-            componentString = Resources.getSystem().getString(
-                    com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent);
-        } else {
-            componentString = Resources.getSystem().getString(
-                    com.android.internal.R.string.config_customAdbWifiNetworkConfirmationComponent);
-        }
+        String componentString =
+                Resources.getSystem().getString(
+                        R.string.config_customAdbWifiNetworkConfirmationComponent);
         ComponentName componentName = ComponentName.unflattenFromString(componentString);
+        UserInfo userInfo = UserManager.get(mContext).getUserInfo(currentUserId);
         if (startConfirmationActivity(componentName, userInfo.getUserHandle(), extras)
-                || startConfirmationService(componentName, userInfo.getUserHandle(),
-                        extras)) {
+                || startConfirmationService(componentName, userInfo.getUserHandle(), extras)) {
             return;
         }
         Slog.e(TAG, "Unable to start customAdbWifiNetworkConfirmation[SecondaryUser]Component "
@@ -1543,7 +1571,7 @@
     /**
      * Returns a new File with the specified name in the adb directory.
      */
-    private File getAdbFile(String fileName) {
+    private static File getAdbFile(String fileName) {
         File dataDir = Environment.getDataDirectory();
         File adbDir = new File(dataDir, ADB_DIRECTORY);
 
@@ -1556,66 +1584,38 @@
     }
 
     File getAdbTempKeysFile() {
-        return getAdbFile(ADB_TEMP_KEYS_FILE);
+        return mTempKeysFile;
     }
 
     File getUserKeyFile() {
-        return mTestUserKeyFile == null ? getAdbFile(ADB_KEYS_FILE) : mTestUserKeyFile;
-    }
-
-    private void writeKey(String key) {
-        try {
-            File keyFile = getUserKeyFile();
-
-            if (keyFile == null) {
-                return;
-            }
-
-            FileOutputStream fo = new FileOutputStream(keyFile, true);
-            fo.write(key.getBytes());
-            fo.write('\n');
-            fo.close();
-
-            FileUtils.setPermissions(keyFile.toString(),
-                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
-        } catch (IOException ex) {
-            Slog.e(TAG, "Error writing key:" + ex);
-        }
+        return mUserKeyFile;
     }
 
     private void writeKeys(Iterable<String> keys) {
-        AtomicFile atomicKeyFile = null;
+        if (mUserKeyFile == null) {
+            return;
+        }
+
+        AtomicFile atomicKeyFile = new AtomicFile(mUserKeyFile);
+        // Note: Do not use a try-with-resources with the FileOutputStream, because AtomicFile
+        // requires that it's cleaned up with AtomicFile.failWrite();
         FileOutputStream fo = null;
         try {
-            File keyFile = getUserKeyFile();
-
-            if (keyFile == null) {
-                return;
-            }
-
-            atomicKeyFile = new AtomicFile(keyFile);
             fo = atomicKeyFile.startWrite();
             for (String key : keys) {
                 fo.write(key.getBytes());
                 fo.write('\n');
             }
             atomicKeyFile.finishWrite(fo);
-
-            FileUtils.setPermissions(keyFile.toString(),
-                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
         } catch (IOException ex) {
             Slog.e(TAG, "Error writing keys: " + ex);
-            if (atomicKeyFile != null) {
-                atomicKeyFile.failWrite(fo);
-            }
+            atomicKeyFile.failWrite(fo);
+            return;
         }
-    }
 
-    private void deleteKeyFile() {
-        File keyFile = getUserKeyFile();
-        if (keyFile != null) {
-            keyFile.delete();
-        }
+        FileUtils.setPermissions(
+                mUserKeyFile.toString(),
+                FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP, -1, -1);
     }
 
     /**
@@ -1745,6 +1745,13 @@
     }
 
     /**
+     * Notify that they key files were updated so the AdbKeyManager reloads the keys.
+     */
+    public void notifyKeyFilesUpdated() {
+        mHandler.sendEmptyMessage(AdbDebuggingHandler.MESSAGE_KEY_FILES_UPDATED);
+    }
+
+    /**
      * Sends a message to the handler to persist the keystore.
      */
     private void sendPersistKeyStoreMessage() {
@@ -1778,7 +1785,7 @@
 
         try {
             dump.write("keystore", AdbDebuggingManagerProto.KEYSTORE,
-                    FileUtils.readTextFile(getAdbTempKeysFile(), 0, null));
+                    FileUtils.readTextFile(mTempKeysFile, 0, null));
         } catch (IOException e) {
             Slog.i(TAG, "Cannot read keystore: ", e);
         }
@@ -1792,12 +1799,12 @@
      * ADB_ALLOWED_CONNECTION_TIME setting.
      */
     class AdbKeyStore {
-        private Map<String, Long> mKeyMap;
-        private Set<String> mSystemKeys;
-        private File mKeyFile;
         private AtomicFile mAtomicKeyFile;
 
-        private List<String> mTrustedNetworks;
+        private final Set<String> mSystemKeys;
+        private final Map<String, Long> mKeyMap = new HashMap<>();
+        private final List<String> mTrustedNetworks = new ArrayList<>();
+
         private static final int KEYSTORE_VERSION = 1;
         private static final int MAX_SUPPORTED_KEYSTORE_VERSION = 1;
         private static final String XML_KEYSTORE_START_TAG = "keyStore";
@@ -1819,26 +1826,22 @@
         public static final long NO_PREVIOUS_CONNECTION = 0;
 
         /**
-         * Constructor that uses the default location for the persistent adb keystore.
+         * Create an AdbKeyStore instance.
+         *
+         * <p>Upon creation, we parse {@link #mTempKeysFile} to determine authorized WiFi APs and
+         * retrieve the map of stored ADB keys and their last connected times. After that, we read
+         * the {@link #mUserKeyFile}, and any keys that exist in that file that do not exist in the
+         * map are added to the map (for backwards compatibility).
          */
         AdbKeyStore() {
-            init();
-        }
-
-        /**
-         * Constructor that uses the specified file as the location for the persistent adb keystore.
-         */
-        AdbKeyStore(File keyFile) {
-            mKeyFile = keyFile;
-            init();
-        }
-
-        private void init() {
             initKeyFile();
-            mKeyMap = getKeyMap();
-            mTrustedNetworks = getTrustedNetworks();
+            readTempKeysFile();
             mSystemKeys = getSystemKeysFromFile(SYSTEM_KEY_FILE);
-            addUserKeysToKeyStore();
+            addExistingUserKeysToKeyStore();
+        }
+
+        public void reloadKeyMap() {
+            readTempKeysFile();
         }
 
         public void addTrustedNetwork(String bssid) {
@@ -1877,7 +1880,6 @@
         public void removeKey(String key) {
             if (mKeyMap.containsKey(key)) {
                 mKeyMap.remove(key);
-                writeKeys(mKeyMap.keySet());
                 sendPersistKeyStoreMessage();
             }
         }
@@ -1886,12 +1888,9 @@
          * Initializes the key file that will be used to persist the adb grants.
          */
         private void initKeyFile() {
-            if (mKeyFile == null) {
-                mKeyFile = getAdbTempKeysFile();
-            }
-            // getAdbTempKeysFile can return null if the adb file cannot be obtained
-            if (mKeyFile != null) {
-                mAtomicKeyFile = new AtomicFile(mKeyFile);
+            // mTempKeysFile can be null if the adb file cannot be obtained
+            if (mTempKeysFile != null) {
+                mAtomicKeyFile = new AtomicFile(mTempKeysFile);
             }
         }
 
@@ -1932,201 +1931,108 @@
         }
 
         /**
-         * Returns the key map with the keys and last connection times from the key file.
+         * Update the key map and the trusted networks list with values parsed from the temp keys
+         * file.
          */
-        private Map<String, Long> getKeyMap() {
-            Map<String, Long> keyMap = new HashMap<String, Long>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
+        private void readTempKeysFile() {
+            mKeyMap.clear();
+            mTrustedNetworks.clear();
             if (mAtomicKeyFile == null) {
                 initKeyFile();
                 if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return keyMap;
+                    Slog.e(
+                            TAG,
+                            "Unable to obtain the key file, " + mTempKeysFile + ", for reading");
+                    return;
                 }
             }
             if (!mAtomicKeyFile.exists()) {
-                return keyMap;
+                return;
             }
             try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                // Check for supported keystore version.
-                XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
-                if (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) {
-                        Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag="
-                                + tagName);
-                        return keyMap;
-                    }
+                TypedXmlPullParser parser;
+                try {
+                    parser = Xml.resolvePullParser(keyStream);
+                    XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
+
                     int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION);
                     if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) {
                         Slog.e(TAG, "Keystore version=" + keystoreVersion
                                 + " not supported (max_supported="
                                 + MAX_SUPPORTED_KEYSTORE_VERSION + ")");
-                        return keyMap;
+                        return;
                     }
+                } catch (XmlPullParserException e) {
+                    // This could be because the XML document doesn't start with
+                    // XML_KEYSTORE_START_TAG. Try again, instead just starting the document with
+                    // the adbKey tag (the old format).
+                    parser = Xml.resolvePullParser(keyStream);
                 }
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_ADB_KEY)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
-                    long connectionTime;
-                    try {
-                        connectionTime = parser.getAttributeLong(null,
-                                XML_ATTRIBUTE_LAST_CONNECTION);
-                    } catch (XmlPullParserException e) {
-                        Slog.e(TAG,
-                                "Caught a NumberFormatException parsing the last connection time: "
-                                        + e);
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    keyMap.put(key, connectionTime);
-                }
+                readKeyStoreContents(parser);
             } catch (IOException e) {
                 Slog.e(TAG, "Caught an IOException parsing the XML key file: ", e);
             } catch (XmlPullParserException e) {
-                Slog.w(TAG, "Caught XmlPullParserException parsing the XML key file: ", e);
-                // The file could be written in a format prior to introducing keystore tag.
-                return getKeyMapBeforeKeystoreVersion();
+                Slog.e(TAG, "Caught XmlPullParserException parsing the XML key file: ", e);
             }
-            return keyMap;
         }
 
-
-        /**
-         * Returns the key map with the keys and last connection times from the key file.
-         * This implementation was prior to adding the XML_KEYSTORE_START_TAG.
-         */
-        private Map<String, Long> getKeyMapBeforeKeystoreVersion() {
-            Map<String, Long> keyMap = new HashMap<String, Long>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
-            if (mAtomicKeyFile == null) {
-                initKeyFile();
-                if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return keyMap;
+        private void readKeyStoreContents(TypedXmlPullParser parser)
+                throws XmlPullParserException, IOException {
+            // This parser is very forgiving. For backwards-compatibility, we simply iterate through
+            // all the tags in the file, skipping over anything that's not an <adbKey> tag or a
+            // <wifiAP> tag. Invalid tags (such as ones that don't have a valid "lastConnection"
+            // attribute) are simply ignored.
+            while ((parser.next()) != XmlPullParser.END_DOCUMENT) {
+                String tagName = parser.getName();
+                if (XML_TAG_ADB_KEY.equals(tagName)) {
+                    addAdbKeyToKeyMap(parser);
+                } else if (XML_TAG_WIFI_ACCESS_POINT.equals(tagName)) {
+                    addTrustedNetworkToTrustedNetworks(parser);
+                } else {
+                    Slog.w(TAG, "Ignoring tag '" + tagName + "'. Not recognized.");
                 }
+                XmlUtils.skipCurrentTag(parser);
             }
-            if (!mAtomicKeyFile.exists()) {
-                return keyMap;
-            }
-            try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                XmlUtils.beginDocument(parser, XML_TAG_ADB_KEY);
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_ADB_KEY)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
-                    long connectionTime;
-                    try {
-                        connectionTime = parser.getAttributeLong(null,
-                                XML_ATTRIBUTE_LAST_CONNECTION);
-                    } catch (XmlPullParserException e) {
-                        Slog.e(TAG,
-                                "Caught a NumberFormatException parsing the last connection time: "
-                                        + e);
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    keyMap.put(key, connectionTime);
-                }
-            } catch (IOException | XmlPullParserException e) {
-                Slog.e(TAG, "Caught an exception parsing the XML key file: ", e);
-            }
-            return keyMap;
         }
 
-        /**
-         * Returns the map of trusted networks from the keystore file.
-         *
-         * This was implemented in keystore version 1.
-         */
-        private List<String> getTrustedNetworks() {
-            List<String> trustedNetworks = new ArrayList<String>();
-            // if the AtomicFile could not be instantiated before attempt again; if it still fails
-            // return an empty key map.
-            if (mAtomicKeyFile == null) {
-                initKeyFile();
-                if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for reading");
-                    return trustedNetworks;
-                }
+        private void addAdbKeyToKeyMap(TypedXmlPullParser parser) {
+            String key = parser.getAttributeValue(null, XML_ATTRIBUTE_KEY);
+            try {
+                long connectionTime =
+                        parser.getAttributeLong(null, XML_ATTRIBUTE_LAST_CONNECTION);
+                mKeyMap.put(key, connectionTime);
+            } catch (XmlPullParserException e) {
+                Slog.e(TAG, "Error reading adbKey attributes", e);
             }
-            if (!mAtomicKeyFile.exists()) {
-                return trustedNetworks;
-            }
-            try (FileInputStream keyStream = mAtomicKeyFile.openRead()) {
-                TypedXmlPullParser parser = Xml.resolvePullParser(keyStream);
-                // Check for supported keystore version.
-                XmlUtils.beginDocument(parser, XML_KEYSTORE_START_TAG);
-                if (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null || !XML_KEYSTORE_START_TAG.equals(tagName)) {
-                        Slog.e(TAG, "Expected " + XML_KEYSTORE_START_TAG + ", but got tag="
-                                + tagName);
-                        return trustedNetworks;
-                    }
-                    int keystoreVersion = parser.getAttributeInt(null, XML_ATTRIBUTE_VERSION);
-                    if (keystoreVersion > MAX_SUPPORTED_KEYSTORE_VERSION) {
-                        Slog.e(TAG, "Keystore version=" + keystoreVersion
-                                + " not supported (max_supported="
-                                + MAX_SUPPORTED_KEYSTORE_VERSION);
-                        return trustedNetworks;
-                    }
-                }
-                while (parser.next() != XmlPullParser.END_DOCUMENT) {
-                    String tagName = parser.getName();
-                    if (tagName == null) {
-                        break;
-                    } else if (!tagName.equals(XML_TAG_WIFI_ACCESS_POINT)) {
-                        XmlUtils.skipCurrentTag(parser);
-                        continue;
-                    }
-                    String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID);
-                    trustedNetworks.add(bssid);
-                }
-            } catch (IOException | XmlPullParserException | NumberFormatException e) {
-                Slog.e(TAG, "Caught an exception parsing the XML key file: ", e);
-            }
-            return trustedNetworks;
+        }
+
+        private void addTrustedNetworkToTrustedNetworks(TypedXmlPullParser parser) {
+            String bssid = parser.getAttributeValue(null, XML_ATTRIBUTE_WIFI_BSSID);
+            mTrustedNetworks.add(bssid);
         }
 
         /**
          * Updates the keystore with keys that were previously set to be always allowed before the
          * connection time of keys was tracked.
          */
-        private void addUserKeysToKeyStore() {
-            File userKeyFile = getUserKeyFile();
+        private void addExistingUserKeysToKeyStore() {
+            if (mUserKeyFile == null || !mUserKeyFile.exists()) {
+                return;
+            }
             boolean mapUpdated = false;
-            if (userKeyFile != null && userKeyFile.exists()) {
-                try (BufferedReader in = new BufferedReader(new FileReader(userKeyFile))) {
-                    long time = System.currentTimeMillis();
-                    String key;
-                    while ((key = in.readLine()) != null) {
-                        // if the keystore does not contain the key from the user key file then add
-                        // it to the Map with the current system time to prevent it from expiring
-                        // immediately if the user is actively using this key.
-                        if (!mKeyMap.containsKey(key)) {
-                            mKeyMap.put(key, time);
-                            mapUpdated = true;
-                        }
+            try (BufferedReader in = new BufferedReader(new FileReader(mUserKeyFile))) {
+                String key;
+                while ((key = in.readLine()) != null) {
+                    // if the keystore does not contain the key from the user key file then add
+                    // it to the Map with the current system time to prevent it from expiring
+                    // immediately if the user is actively using this key.
+                    if (!mKeyMap.containsKey(key)) {
+                        mKeyMap.put(key, mTicker.currentTimeMillis());
+                        mapUpdated = true;
                     }
-                } catch (IOException e) {
-                    Slog.e(TAG, "Caught an exception reading " + userKeyFile + ": " + e);
                 }
+            } catch (IOException e) {
+                Slog.e(TAG, "Caught an exception reading " + mUserKeyFile + ": " + e);
             }
             if (mapUpdated) {
                 sendPersistKeyStoreMessage();
@@ -2147,7 +2053,9 @@
             if (mAtomicKeyFile == null) {
                 initKeyFile();
                 if (mAtomicKeyFile == null) {
-                    Slog.e(TAG, "Unable to obtain the key file, " + mKeyFile + ", for writing");
+                    Slog.e(
+                            TAG,
+                            "Unable to obtain the key file, " + mTempKeysFile + ", for writing");
                     return;
                 }
             }
@@ -2178,17 +2086,21 @@
                 Slog.e(TAG, "Caught an exception writing the key map: ", e);
                 mAtomicKeyFile.failWrite(keyStream);
             }
+            writeKeys(mKeyMap.keySet());
         }
 
         private boolean filterOutOldKeys() {
-            boolean keysDeleted = false;
             long allowedTime = getAllowedConnectionTime();
-            long systemTime = System.currentTimeMillis();
+            if (allowedTime == 0) {
+                return false;
+            }
+            boolean keysDeleted = false;
+            long systemTime = mTicker.currentTimeMillis();
             Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
             while (keyMapIterator.hasNext()) {
                 Map.Entry<String, Long> keyEntry = keyMapIterator.next();
                 long connectionTime = keyEntry.getValue();
-                if (allowedTime != 0 && systemTime > (connectionTime + allowedTime)) {
+                if (systemTime > (connectionTime + allowedTime)) {
                     keyMapIterator.remove();
                     keysDeleted = true;
                 }
@@ -2212,7 +2124,7 @@
             if (allowedTime == 0) {
                 return minExpiration;
             }
-            long systemTime = System.currentTimeMillis();
+            long systemTime = mTicker.currentTimeMillis();
             Iterator<Map.Entry<String, Long>> keyMapIterator = mKeyMap.entrySet().iterator();
             while (keyMapIterator.hasNext()) {
                 Map.Entry<String, Long> keyEntry = keyMapIterator.next();
@@ -2233,7 +2145,9 @@
         public void deleteKeyStore() {
             mKeyMap.clear();
             mTrustedNetworks.clear();
-            deleteKeyFile();
+            if (mUserKeyFile != null) {
+                mUserKeyFile.delete();
+            }
             if (mAtomicKeyFile == null) {
                 return;
             }
@@ -2260,7 +2174,8 @@
          * is set to true the time will be set even if it is older than the previously written
          * connection time.
          */
-        public void setLastConnectionTime(String key, long connectionTime, boolean force) {
+        @VisibleForTesting
+        void setLastConnectionTime(String key, long connectionTime, boolean force) {
             // Do not set the connection time to a value that is earlier than what was previously
             // stored as the last connection time unless force is set.
             if (mKeyMap.containsKey(key) && mKeyMap.get(key) >= connectionTime && !force) {
@@ -2271,11 +2186,6 @@
             if (mSystemKeys.contains(key)) {
                 return;
             }
-            // if this is the first time the key is being added then write it to the key file as
-            // well.
-            if (!mKeyMap.containsKey(key)) {
-                writeKey(key);
-            }
             mKeyMap.put(key, connectionTime);
         }
 
@@ -2307,12 +2217,8 @@
             long allowedConnectionTime = getAllowedConnectionTime();
             // if the allowed connection time is 0 then revert to the previous behavior of always
             // allowing previously granted adb grants.
-            if (allowedConnectionTime == 0 || (System.currentTimeMillis() < (lastConnectionTime
-                    + allowedConnectionTime))) {
-                return true;
-            } else {
-                return false;
-            }
+            return allowedConnectionTime == 0
+                    || (mTicker.currentTimeMillis() < (lastConnectionTime + allowedConnectionTime));
         }
 
         /**
@@ -2324,4 +2230,15 @@
             return mTrustedNetworks.contains(bssid);
         }
     }
+
+    /**
+     * A Guava-like interface for getting the current system time.
+     *
+     * This allows us to swap a fake ticker in for testing to reduce "Thread.sleep()" calls and test
+     * for exact expected times instead of random ones.
+     */
+    @VisibleForTesting
+    interface Ticker {
+        long currentTimeMillis();
+    }
 }
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index 5d0c732..55d8dba 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -152,6 +152,14 @@
         }
 
         @Override
+        public void notifyKeyFilesUpdated() {
+            if (mDebuggingManager == null) {
+                return;
+            }
+            mDebuggingManager.notifyKeyFilesUpdated();
+        }
+
+        @Override
         public void startAdbdForTransport(byte transportType) {
             FgThread.getHandler().sendMessage(obtainMessage(
                     AdbService::setAdbdEnabledForTransport, AdbService.this, true, transportType));
diff --git a/services/core/java/com/android/server/am/TraceErrorLogger.java b/services/core/java/com/android/server/am/TraceErrorLogger.java
index 29a9b5c..ec0587f 100644
--- a/services/core/java/com/android/server/am/TraceErrorLogger.java
+++ b/services/core/java/com/android/server/am/TraceErrorLogger.java
@@ -16,7 +16,6 @@
 
 package com.android.server.am;
 
-import android.os.Build;
 import android.os.Trace;
 
 import java.util.UUID;
@@ -31,7 +30,7 @@
     private static final int PLACEHOLDER_VALUE = 1;
 
     public boolean isAddErrorIdEnabled() {
-        return Build.IS_DEBUGGABLE;
+        return true;
     }
 
     /**
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 43d77ab..0040ea9 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -365,6 +365,8 @@
     private static final int MSG_REMOVE_ASSISTANT_SERVICE_UID = 45;
     private static final int MSG_UPDATE_ACTIVE_ASSISTANT_SERVICE_UID = 46;
     private static final int MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR = 47;
+    private static final int MSG_ROTATION_UPDATE = 48;
+    private static final int MSG_FOLD_UPDATE = 49;
 
     // start of messages handled under wakelock
     //   these messages can only be queued, i.e. sent with queueMsgUnderWakeLock(),
@@ -1251,7 +1253,9 @@
 
         intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
         if (mMonitorRotation) {
-            RotationHelper.init(mContext, mAudioHandler);
+            RotationHelper.init(mContext, mAudioHandler,
+                    rotationParam -> onRotationUpdate(rotationParam),
+                    foldParam -> onFoldUpdate(foldParam));
         }
 
         intentFilter.addAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
@@ -1398,6 +1402,20 @@
     }
 
     //-----------------------------------------------------------------
+    // rotation/fold updates coming from RotationHelper
+    void onRotationUpdate(String rotationParameter) {
+        // use REPLACE as only the last rotation matters
+        sendMsg(mAudioHandler, MSG_ROTATION_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0,
+                /*obj*/ rotationParameter, /*delay*/ 0);
+    }
+
+    void onFoldUpdate(String foldParameter) {
+        // use REPLACE as only the last fold state matters
+        sendMsg(mAudioHandler, MSG_FOLD_UPDATE, SENDMSG_REPLACE, /*arg1*/ 0, /*arg2*/ 0,
+                /*obj*/ foldParameter, /*delay*/ 0);
+    }
+
+    //-----------------------------------------------------------------
     // monitoring requests for volume range initialization
     @Override // AudioSystemAdapter.OnVolRangeInitRequestListener
     public void onVolumeRangeInitRequestFromNative() {
@@ -8327,6 +8345,16 @@
                 case MSG_DISPATCH_DEVICE_VOLUME_BEHAVIOR:
                     dispatchDeviceVolumeBehavior((AudioDeviceAttributes) msg.obj, msg.arg1);
                     break;
+
+                case MSG_ROTATION_UPDATE:
+                    // rotation parameter format: "rotation=x" where x is one of 0, 90, 180, 270
+                    mAudioSystem.setParameters((String) msg.obj);
+                    break;
+
+                case MSG_FOLD_UPDATE:
+                    // fold parameter format: "device_folded=x" where x is one of on, off
+                    mAudioSystem.setParameters((String) msg.obj);
+                    break;
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/RotationHelper.java b/services/core/java/com/android/server/audio/RotationHelper.java
index eb8387f..5cdf58b 100644
--- a/services/core/java/com/android/server/audio/RotationHelper.java
+++ b/services/core/java/com/android/server/audio/RotationHelper.java
@@ -21,13 +21,14 @@
 import android.hardware.devicestate.DeviceStateManager.FoldStateListener;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerGlobal;
-import android.media.AudioSystem;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.util.Log;
 import android.view.Display;
 import android.view.Surface;
 
+import java.util.function.Consumer;
+
 /**
  * Class to handle device rotation events for AudioService, and forward device rotation
  * and folded state to the audio HALs through AudioSystem.
@@ -53,6 +54,10 @@
 
     private static AudioDisplayListener sDisplayListener;
     private static FoldStateListener sFoldStateListener;
+    /** callback to send rotation updates to AudioSystem */
+    private static Consumer<String> sRotationUpdateCb;
+    /** callback to send folded state updates to AudioSystem */
+    private static Consumer<String> sFoldUpdateCb;
 
     private static final Object sRotationLock = new Object();
     private static final Object sFoldStateLock = new Object();
@@ -67,13 +72,16 @@
      * - sDisplayListener != null
      * - sContext != null
      */
-    static void init(Context context, Handler handler) {
+    static void init(Context context, Handler handler,
+            Consumer<String> rotationUpdateCb, Consumer<String> foldUpdateCb) {
         if (context == null) {
             throw new IllegalArgumentException("Invalid null context");
         }
         sContext = context;
         sHandler = handler;
         sDisplayListener = new AudioDisplayListener();
+        sRotationUpdateCb = rotationUpdateCb;
+        sFoldUpdateCb = foldUpdateCb;
         enable();
     }
 
@@ -115,21 +123,26 @@
         if (DEBUG_ROTATION) {
             Log.i(TAG, "publishing device rotation =" + rotation + " (x90deg)");
         }
+        String rotationParam;
         switch (rotation) {
             case Surface.ROTATION_0:
-                AudioSystem.setParameters("rotation=0");
+                rotationParam = "rotation=0";
                 break;
             case Surface.ROTATION_90:
-                AudioSystem.setParameters("rotation=90");
+                rotationParam = "rotation=90";
                 break;
             case Surface.ROTATION_180:
-                AudioSystem.setParameters("rotation=180");
+                rotationParam = "rotation=180";
                 break;
             case Surface.ROTATION_270:
-                AudioSystem.setParameters("rotation=270");
+                rotationParam = "rotation=270";
                 break;
             default:
                 Log.e(TAG, "Unknown device rotation");
+                rotationParam = null;
+        }
+        if (rotationParam != null) {
+            sRotationUpdateCb.accept(rotationParam);
         }
     }
 
@@ -140,11 +153,13 @@
         synchronized (sFoldStateLock) {
             if (sDeviceFold != newFolded) {
                 sDeviceFold = newFolded;
+                String foldParam;
                 if (newFolded) {
-                    AudioSystem.setParameters("device_folded=on");
+                    foldParam = "device_folded=on";
                 } else {
-                    AudioSystem.setParameters("device_folded=off");
+                    foldParam = "device_folded=off";
                 }
+                sFoldUpdateCb.accept(foldParam);
             }
         }
     }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 5b26672..dd44af1 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -280,18 +280,13 @@
             }
             // for both transaural / binaural, we are not forcing enablement as the init() method
             // could have been called another time after boot in case of audioserver restart
-            if (mTransauralSupported) {
-                // not force-enabling as this device might already be in the device list
-                addCompatibleAudioDevice(
-                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
-                                false /*forceEnable*/);
-            }
-            if (mBinauralSupported) {
-                // not force-enabling as this device might already be in the device list
-                addCompatibleAudioDevice(
-                        new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
-                                false /*forceEnable*/);
-            }
+            addCompatibleAudioDevice(
+                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_SPEAKER, ""),
+                            false /*forceEnable*/);
+            // not force-enabling as this device might already be in the device list
+            addCompatibleAudioDevice(
+                    new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE, ""),
+                            false /*forceEnable*/);
         } catch (RemoteException e) {
             resetCapabilities();
         } finally {
@@ -497,10 +492,9 @@
     synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
         // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices
         ArrayList<AudioDeviceAttributes> compatList = new ArrayList<>();
-        for (SADeviceState dev : mSADevices) {
-            if (dev.mEnabled) {
-                compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
-                        dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress));
+        for (SADeviceState deviceState : mSADevices) {
+            if (deviceState.mEnabled) {
+                compatList.add(deviceState.getAudioDeviceAttributes());
             }
         }
         return compatList;
@@ -521,15 +515,15 @@
      */
     private void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada,
             boolean forceEnable) {
+        if (!isDeviceCompatibleWithSpatializationModes(ada)) {
+            return;
+        }
         loglogi("addCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         boolean isInList = false;
         SADeviceState deviceUpdated = null; // non-null on update.
 
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 isInList = true;
                 if (forceEnable) {
                     deviceState.mEnabled = true;
@@ -539,11 +533,10 @@
             }
         }
         if (!isInList) {
-            final SADeviceState dev = new SADeviceState(deviceType,
-                    wireless ? ada.getAddress() : "");
-            dev.mEnabled = true;
-            mSADevices.add(dev);
-            deviceUpdated = dev;
+            final SADeviceState deviceState = new SADeviceState(ada.getType(), ada.getAddress());
+            deviceState.mEnabled = true;
+            mSADevices.add(deviceState);
+            deviceUpdated = deviceState;
         }
         if (deviceUpdated != null) {
             onRoutingUpdated();
@@ -574,13 +567,10 @@
 
     synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
         loglogi("removeCompatibleAudioDevice: dev=" + ada);
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         SADeviceState deviceUpdated = null; // non-null on update.
 
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (!wireless || ada.getAddress().equals(deviceState.mDeviceAddress))) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 deviceState.mEnabled = false;
                 deviceUpdated = deviceState;
                 break;
@@ -602,10 +592,9 @@
         // if not a wireless device, this value will be overwritten to map the type
         // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES
         @AudioDeviceInfo.AudioDeviceType int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
 
         // if not a wireless device: find if media device is in the speaker, wired headphones
-        if (!wireless) {
+        if (!isWireless(deviceType)) {
             // is the device type capable of doing SA?
             if (!mSACapableDeviceTypes.contains(deviceType)) {
                 Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada);
@@ -640,9 +629,7 @@
         boolean enabled = false;
         boolean available = false;
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 available = true;
                 enabled = deviceState.mEnabled;
                 break;
@@ -652,11 +639,12 @@
     }
 
     private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) {
+        if (!isDeviceCompatibleWithSpatializationModes(ada)) {
+            return;
+        }
         boolean knownDevice = false;
         for (SADeviceState deviceState : mSADevices) {
-            // wireless device so always check address
-            if (ada.getType() == deviceState.mDeviceType
-                    && ada.getAddress().equals(deviceState.mDeviceAddress)) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 knownDevice = true;
                 break;
             }
@@ -704,13 +692,8 @@
         if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
             return false;
         }
-
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 return true;
             }
         }
@@ -719,12 +702,19 @@
 
     private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes,
             @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) {
-        final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(devices[0].getType(),
+        if (isDeviceCompatibleWithSpatializationModes(devices[0])) {
+            return AudioSystem.canBeSpatialized(attributes, format, devices);
+        }
+        return false;
+    }
+
+    private boolean isDeviceCompatibleWithSpatializationModes(@NonNull AudioDeviceAttributes ada) {
+        final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(ada.getType(),
                 /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL);
         if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported)
                 || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL
                         && mTransauralSupported)) {
-            return AudioSystem.canBeSpatialized(attributes, format, devices);
+            return true;
         }
         return false;
     }
@@ -1089,13 +1079,8 @@
             Log.v(TAG, "no headtracking support, ignoring setHeadTrackerEnabled to " + enabled
                     + " for " + ada);
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled
                             + " device:" + ada + " on a device without headtracker");
@@ -1109,7 +1094,7 @@
             }
         }
         // check current routing to see if it affects the headtracking mode
-        if (ROUTING_DEVICES[0].getType() == deviceType
+        if (ROUTING_DEVICES[0].getType() == ada.getType()
                 && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) {
             setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled
                     : Spatializer.HEAD_TRACKING_MODE_DISABLED);
@@ -1121,13 +1106,8 @@
             Log.v(TAG, "no headtracking support, hasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 return deviceState.mHasHeadTracker;
             }
         }
@@ -1144,13 +1124,8 @@
             Log.v(TAG, "no headtracking support, setHasHeadTracker always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     deviceState.mHasHeadTracker = true;
                     mAudioService.persistSpatialAudioDeviceSettings();
@@ -1168,13 +1143,8 @@
             Log.v(TAG, "no headtracking support, isHeadTrackerEnabled always false for " + ada);
             return false;
         }
-        final int deviceType = ada.getType();
-        final boolean wireless = isWireless(deviceType);
-
         for (SADeviceState deviceState : mSADevices) {
-            if (deviceType == deviceState.mDeviceType
-                    && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress))
-                    || !wireless) {
+            if (deviceState.matchesAudioDeviceAttributes(ada)) {
                 if (!deviceState.mHasHeadTracker) {
                     return false;
                 }
@@ -1531,7 +1501,7 @@
 
         SADeviceState(@AudioDeviceInfo.AudioDeviceType int deviceType, @NonNull String address) {
             mDeviceType = deviceType;
-            mDeviceAddress = Objects.requireNonNull(address);
+            mDeviceAddress = isWireless(deviceType) ? Objects.requireNonNull(address) : "";
         }
 
         @Override
@@ -1599,6 +1569,18 @@
                 return null;
             }
         }
+
+        public AudioDeviceAttributes getAudioDeviceAttributes() {
+            return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    mDeviceType, mDeviceAddress == null ? "" : mDeviceAddress);
+        }
+
+        public boolean matchesAudioDeviceAttributes(AudioDeviceAttributes ada) {
+            final int deviceType = ada.getType();
+            final boolean wireless = isWireless(deviceType);
+            return (deviceType == mDeviceType)
+                        && (!wireless || ada.getAddress().equals(mDeviceAddress));
+        }
     }
 
     /*package*/ synchronized String getSADeviceSettings() {
@@ -1619,7 +1601,9 @@
         // small list, not worth overhead of Arrays.stream(devSettings)
         for (String setting : devSettings) {
             SADeviceState devState = SADeviceState.fromPersistedString(setting);
-            if (devState != null) {
+            if (devState != null
+                    && isDeviceCompatibleWithSpatializationModes(
+                            devState.getAudioDeviceAttributes())) {
                 mSADevices.add(devState);
                 logDeviceState(devState, "setSADeviceSettings");
             }
diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java
index cc49f07..41ca13f 100644
--- a/services/core/java/com/android/server/biometrics/AuthSession.java
+++ b/services/core/java/com/android/server/biometrics/AuthSession.java
@@ -538,13 +538,12 @@
 
     void onDialogAnimatedIn() {
         if (mState != STATE_AUTH_STARTED) {
-            Slog.w(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
+            Slog.e(TAG, "onDialogAnimatedIn, unexpected state: " + mState);
+            return;
         }
 
         mState = STATE_AUTH_STARTED_UI_SHOWING;
-
         startAllPreparedFingerprintSensors();
-        mState = STATE_AUTH_STARTED_UI_SHOWING;
     }
 
     void onTryAgainPressed() {
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index 968146a..ef2931f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -20,14 +20,18 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.hardware.biometrics.BiometricConstants;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.function.BooleanSupplier;
 
 /**
  * Contains all the necessary information for a HAL operation.
@@ -84,6 +88,8 @@
     private final BaseClientMonitor mClientMonitor;
     @Nullable
     private final ClientMonitorCallback mClientCallback;
+    @NonNull
+    private final BooleanSupplier mIsDebuggable;
     @Nullable
     private ClientMonitorCallback mOnStartCallback;
     @OperationState
@@ -99,14 +105,33 @@
         this(clientMonitor, callback, STATE_WAITING_IN_QUEUE);
     }
 
+    @VisibleForTesting
+    BiometricSchedulerOperation(
+            @NonNull BaseClientMonitor clientMonitor,
+            @Nullable ClientMonitorCallback callback,
+            @NonNull BooleanSupplier isDebuggable
+    ) {
+        this(clientMonitor, callback, STATE_WAITING_IN_QUEUE, isDebuggable);
+    }
+
     protected BiometricSchedulerOperation(
             @NonNull BaseClientMonitor clientMonitor,
             @Nullable ClientMonitorCallback callback,
             @OperationState int state
     ) {
+        this(clientMonitor, callback, state, Build::isDebuggable);
+    }
+
+    private BiometricSchedulerOperation(
+            @NonNull BaseClientMonitor clientMonitor,
+            @Nullable ClientMonitorCallback callback,
+            @OperationState int state,
+            @NonNull BooleanSupplier isDebuggable
+    ) {
         mClientMonitor = clientMonitor;
         mClientCallback = callback;
         mState = state;
+        mIsDebuggable = isDebuggable;
         mCancelWatchdog = () -> {
             if (!isFinished()) {
                 Slog.e(TAG, "[Watchdog Triggered]: " + this);
@@ -144,13 +169,19 @@
      * @return if this operation started
      */
     public boolean start(@NonNull ClientMonitorCallback callback) {
-        checkInState("start",
+        if (errorWhenNoneOf("start",
                 STATE_WAITING_IN_QUEUE,
                 STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return false;
+        }
 
         if (mClientMonitor.getCookie() != 0) {
-            throw new IllegalStateException("operation requires cookie");
+            String err = "operation requires cookie";
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
+            }
+            Slog.e(TAG, err);
         }
 
         return doStart(callback);
@@ -164,16 +195,18 @@
      * @return if this operation started
      */
     public boolean startWithCookie(@NonNull ClientMonitorCallback callback, int cookie) {
-        checkInState("start",
-                STATE_WAITING_IN_QUEUE,
-                STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
-
         if (mClientMonitor.getCookie() != cookie) {
             Slog.e(TAG, "Mismatched cookie for operation: " + this + ", received: " + cookie);
             return false;
         }
 
+        if (errorWhenNoneOf("start",
+                STATE_WAITING_IN_QUEUE,
+                STATE_WAITING_FOR_COOKIE,
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return false;
+        }
+
         return doStart(callback);
     }
 
@@ -217,10 +250,12 @@
      * immediately abort the operation and notify the client that it has finished unsuccessfully.
      */
     public void abort() {
-        checkInState("cannot abort a non-pending operation",
+        if (errorWhenNoneOf("abort",
                 STATE_WAITING_IN_QUEUE,
                 STATE_WAITING_FOR_COOKIE,
-                STATE_WAITING_IN_QUEUE_CANCELING);
+                STATE_WAITING_IN_QUEUE_CANCELING)) {
+            return;
+        }
 
         if (isHalOperation()) {
             ((HalClientMonitor<?>) mClientMonitor).unableToStart();
@@ -247,7 +282,9 @@
      *                 the callback used from {@link #start(ClientMonitorCallback)} is used)
      */
     public void cancel(@NonNull Handler handler, @NonNull ClientMonitorCallback callback) {
-        checkNotInState("cancel", STATE_FINISHED);
+        if (errorWhenOneOf("cancel", STATE_FINISHED)) {
+            return;
+        }
 
         final int currentState = mState;
         if (!isInterruptable()) {
@@ -402,21 +439,28 @@
         return mClientMonitor;
     }
 
-    private void checkNotInState(String message, @OperationState int... states) {
-        for (int state : states) {
-            if (mState == state) {
-                throw new IllegalStateException(message + ": illegal state= " + state);
+    private boolean errorWhenOneOf(String op, @OperationState int... states) {
+        final boolean isError = ArrayUtils.contains(states, mState);
+        if (isError) {
+            String err = op + ": mState must not be " + mState;
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
             }
+            Slog.e(TAG, err);
         }
+        return isError;
     }
 
-    private void checkInState(String message, @OperationState int... states) {
-        for (int state : states) {
-            if (mState == state) {
-                return;
+    private boolean errorWhenNoneOf(String op, @OperationState int... states) {
+        final boolean isError = !ArrayUtils.contains(states, mState);
+        if (isError) {
+            String err = op + ": mState=" + mState + " must be one of " + Arrays.toString(states);
+            if (mIsDebuggable.getAsBoolean()) {
+                throw new IllegalStateException(err);
             }
+            Slog.e(TAG, err);
         }
-        throw new IllegalStateException(message + ": illegal state= " + mState);
+        return isError;
     }
 
     @Override
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 77d3392..f526960 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -86,6 +86,8 @@
 import android.net.ipsec.ike.ChildSessionParams;
 import android.net.ipsec.ike.IkeSession;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
 import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTunnelConnectionParams;
 import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
@@ -168,9 +170,10 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -190,11 +193,19 @@
     private static final long VPN_LAUNCH_IDLE_ALLOWLIST_DURATION_MS = 60 * 1000;
 
     // Length of time (in milliseconds) that an app registered for VpnManager events is placed on
-    // the device idle allowlist each time the a VpnManager event is fired.
+    // the device idle allowlist each time the VpnManager event is fired.
     private static final long VPN_MANAGER_EVENT_ALLOWLIST_DURATION_MS = 30 * 1000;
 
     private static final String LOCKDOWN_ALLOWLIST_SETTING_NAME =
             Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN_WHITELIST;
+
+    /**
+     * The retries for consecutive failures.
+     *
+     * <p>If retries have exceeded the length of this array, the last entry in the array will be
+     * used as a repeating interval.
+     */
+    private static final long[] IKEV2_VPN_RETRY_DELAYS_SEC = {1L, 2L, 5L, 30L, 60L, 300L, 900L};
     /**
      * Largest profile size allowable for Platform VPNs.
      *
@@ -473,6 +484,20 @@
                         "Cannot set tunnel's fd as blocking=" + blocking, e);
             }
         }
+
+        /**
+         * Retrieves the next retry delay
+         *
+         * <p>If retries have exceeded the IKEV2_VPN_RETRY_DELAYS_SEC, the last entry in
+         * the array will be used as a repeating interval.
+         */
+        public long getNextRetryDelaySeconds(int retryCount) {
+            if (retryCount >= IKEV2_VPN_RETRY_DELAYS_SEC.length) {
+                return IKEV2_VPN_RETRY_DELAYS_SEC[IKEV2_VPN_RETRY_DELAYS_SEC.length - 1];
+            } else {
+                return IKEV2_VPN_RETRY_DELAYS_SEC[retryCount];
+            }
+        }
     }
 
     public Vpn(Looper looper, Context context, INetworkManagementService netService, INetd netd,
@@ -2605,13 +2630,23 @@
 
         void onDefaultNetworkLinkPropertiesChanged(@NonNull LinkProperties lp);
 
-        void onChildOpened(
-                @NonNull Network network, @NonNull ChildSessionConfiguration childConfig);
+        void onDefaultNetworkLost(@NonNull Network network);
 
-        void onChildTransformCreated(
-                @NonNull Network network, @NonNull IpSecTransform transform, int direction);
+        void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration);
 
-        void onSessionLost(@NonNull Network network, @Nullable Exception exception);
+        void onIkeConnectionInfoChanged(
+                int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo);
+
+        void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig);
+
+        void onChildTransformCreated(int token, @NonNull IpSecTransform transform, int direction);
+
+        void onChildMigrated(
+                int token,
+                @NonNull IpSecTransform inTransform,
+                @NonNull IpSecTransform outTransform);
+
+        void onSessionLost(int token, @Nullable Exception exception);
     }
 
     /**
@@ -2642,6 +2677,10 @@
     class IkeV2VpnRunner extends VpnRunner implements IkeV2VpnRunnerCallback {
         @NonNull private static final String TAG = "IkeV2VpnRunner";
 
+        // 5 seconds grace period before tearing down the IKE Session in case new default network
+        // will come up
+        private static final long NETWORK_LOST_TIMEOUT_MS = 5000L;
+
         @NonNull private final IpSecManager mIpSecManager;
         @NonNull private final Ikev2VpnProfile mProfile;
         @NonNull private final ConnectivityManager.NetworkCallback mNetworkCallback;
@@ -2653,24 +2692,60 @@
          * of the mutable Ikev2VpnRunner fields. The Ikev2VpnRunner is built mostly lock-free by
          * virtue of everything being serialized on this executor.
          */
-        @NonNull private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+        @NonNull
+        private final ScheduledThreadPoolExecutor mExecutor = new ScheduledThreadPoolExecutor(1);
+
+        @Nullable private ScheduledFuture<?> mScheduledHandleNetworkLostTimeout;
+        @Nullable private ScheduledFuture<?> mScheduledHandleRetryIkeSessionTimeout;
 
         /** Signal to ensure shutdown is honored even if a new Network is connected. */
         private boolean mIsRunning = true;
 
+        /**
+         * The token used by the primary/current/active IKE session.
+         *
+         * <p>This token MUST be updated when the VPN switches to use a new IKE session.
+         */
+        private int mCurrentToken = -1;
+
         @Nullable private IpSecTunnelInterface mTunnelIface;
-        @Nullable private IkeSession mSession;
         @Nullable private Network mActiveNetwork;
         @Nullable private NetworkCapabilities mUnderlyingNetworkCapabilities;
         @Nullable private LinkProperties mUnderlyingLinkProperties;
         private final String mSessionKey;
 
+        @Nullable private IkeSession mSession;
+        @Nullable private IkeSessionConnectionInfo mIkeConnectionInfo;
+
+        // mMobikeEnabled can only be updated after IKE AUTH is finished.
+        private boolean mMobikeEnabled = false;
+
+        /**
+         * The number of attempts since the last successful connection.
+         *
+         * <p>This variable controls the retry delay, and is reset when a new IKE session is
+         * opened or when there is a new default network.
+         */
+        private int mRetryCount = 0;
+
         IkeV2VpnRunner(@NonNull Ikev2VpnProfile profile) {
             super(TAG);
             mProfile = profile;
             mIpSecManager = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
             mNetworkCallback = new VpnIkev2Utils.Ikev2VpnNetworkCallback(TAG, this, mExecutor);
             mSessionKey = UUID.randomUUID().toString();
+
+            // Set the policy so that cancelled tasks will be removed from the work queue
+            mExecutor.setRemoveOnCancelPolicy(true);
+
+            // Set the policy so that all delayed tasks will not be executed
+            mExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+
+            // To avoid hitting RejectedExecutionException upon shutdown of the mExecutor */
+            mExecutor.setRejectedExecutionHandler(
+                    (r, executor) -> {
+                        Log.d(TAG, "Runnable " + r + " rejected by the mExecutor");
+                    });
         }
 
         @Override
@@ -2709,22 +2784,64 @@
             return Objects.equals(mActiveNetwork, network) && mIsRunning;
         }
 
+        private boolean isActiveToken(int token) {
+            return (mCurrentToken == token) && mIsRunning;
+        }
+
+        /**
+         * Called when an IKE session has been opened
+         *
+         * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor
+         * thread in order to ensure consistency of the Ikev2VpnRunner fields.
+         */
+        public void onIkeOpened(int token, @NonNull IkeSessionConfiguration ikeConfiguration) {
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "onIkeOpened called for obsolete token " + token);
+                return;
+            }
+
+            mMobikeEnabled =
+                    ikeConfiguration.isIkeExtensionEnabled(
+                            IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE);
+            onIkeConnectionInfoChanged(token, ikeConfiguration.getIkeSessionConnectionInfo());
+            mRetryCount = 0;
+        }
+
+        /**
+         * Called when an IKE session's {@link IkeSessionConnectionInfo} is available or updated
+         *
+         * <p>This callback is usually fired when an IKE session has been opened or migrated.
+         *
+         * <p>This method is called multiple times over the lifetime of an IkeSession, and MUST run
+         * on the mExecutor thread in order to ensure consistency of the Ikev2VpnRunner fields.
+         */
+        public void onIkeConnectionInfoChanged(
+                int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) {
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "onIkeConnectionInfoChanged called for obsolete token " + token);
+                return;
+            }
+
+            // The update on VPN and the IPsec tunnel will be done when migration is fully complete
+            // in onChildMigrated
+            mIkeConnectionInfo = ikeConnectionInfo;
+        }
+
         /**
          * Called when an IKE Child session has been opened, signalling completion of the startup.
          *
          * <p>This method is only ever called once per IkeSession, and MUST run on the mExecutor
          * thread in order to ensure consistency of the Ikev2VpnRunner fields.
          */
-        public void onChildOpened(
-                @NonNull Network network, @NonNull ChildSessionConfiguration childConfig) {
-            if (!isActiveNetwork(network)) {
-                Log.d(TAG, "onOpened called for obsolete network " + network);
+        public void onChildOpened(int token, @NonNull ChildSessionConfiguration childConfig) {
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "onChildOpened called for obsolete token " + token);
 
                 // Do nothing; this signals that either: (1) a new/better Network was found,
-                // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this
-                // IKE session was already shut down (exited, or an error was encountered somewhere
-                // else). In both cases, all resources and sessions are torn down via
-                // resetIkeState().
+                // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in
+                // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited,
+                // or an error was encountered somewhere else). In both cases, all resources and
+                // sessions are torn down via resetIkeState().
                 return;
             }
 
@@ -2743,6 +2860,11 @@
                     dnsAddrStrings.add(addr.getHostAddress());
                 }
 
+                // The actual network of this IKE session has been set up with is
+                // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because
+                // mActiveNetwork might have been updated after the setup was triggered.
+                final Network network = mIkeConnectionInfo.getNetwork();
+
                 final NetworkAgent networkAgent;
                 final LinkProperties lp;
 
@@ -2785,8 +2907,8 @@
 
                 networkAgent.sendLinkProperties(lp);
             } catch (Exception e) {
-                Log.d(TAG, "Error in ChildOpened for network " + network, e);
-                onSessionLost(network, e);
+                Log.d(TAG, "Error in ChildOpened for token " + token, e);
+                onSessionLost(token, e);
             }
         }
 
@@ -2794,19 +2916,19 @@
          * Called when an IPsec transform has been created, and should be applied.
          *
          * <p>This method is called multiple times over the lifetime of an IkeSession (or default
-         * network), and is MUST always be called on the mExecutor thread in order to ensure
+         * network), and MUST always be called on the mExecutor thread in order to ensure
          * consistency of the Ikev2VpnRunner fields.
          */
         public void onChildTransformCreated(
-                @NonNull Network network, @NonNull IpSecTransform transform, int direction) {
-            if (!isActiveNetwork(network)) {
-                Log.d(TAG, "ChildTransformCreated for obsolete network " + network);
+                int token, @NonNull IpSecTransform transform, int direction) {
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "ChildTransformCreated for obsolete token " + token);
 
                 // Do nothing; this signals that either: (1) a new/better Network was found,
-                // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this
-                // IKE session was already shut down (exited, or an error was encountered somewhere
-                // else). In both cases, all resources and sessions are torn down via
-                // resetIkeState().
+                // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in
+                // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited,
+                // or an error was encountered somewhere else). In both cases, all resources and
+                // sessions are torn down via resetIkeState().
                 return;
             }
 
@@ -2815,36 +2937,127 @@
                 // them alive for us
                 mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform);
             } catch (IOException e) {
-                Log.d(TAG, "Transform application failed for network " + network, e);
-                onSessionLost(network, e);
+                Log.d(TAG, "Transform application failed for token " + token, e);
+                onSessionLost(token, e);
+            }
+        }
+
+        /**
+         * Called when an IPsec transform has been created, and should be re-applied.
+         *
+         * <p>This method is called multiple times over the lifetime of an IkeSession (or default
+         * network), and MUST always be called on the mExecutor thread in order to ensure
+         * consistency of the Ikev2VpnRunner fields.
+         */
+        public void onChildMigrated(
+                int token,
+                @NonNull IpSecTransform inTransform,
+                @NonNull IpSecTransform outTransform) {
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "onChildMigrated for obsolete token " + token);
+                return;
+            }
+
+            // The actual network of this IKE session has migrated to is
+            // mIkeConnectionInfo.getNetwork() instead of mActiveNetwork because mActiveNetwork
+            // might have been updated after the migration was triggered.
+            final Network network = mIkeConnectionInfo.getNetwork();
+
+            try {
+                synchronized (Vpn.this) {
+                    mConfig.underlyingNetworks = new Network[] {network};
+                    mNetworkCapabilities =
+                            new NetworkCapabilities.Builder(mNetworkCapabilities)
+                                    .setUnderlyingNetworks(Collections.singletonList(network))
+                                    .build();
+                    mNetworkAgent.setUnderlyingNetworks(Collections.singletonList(network));
+                }
+
+                mTunnelIface.setUnderlyingNetwork(network);
+
+                // Transforms do not need to be persisted; the IkeSession will keep them alive for
+                // us
+                mIpSecManager.applyTunnelModeTransform(
+                        mTunnelIface, IpSecManager.DIRECTION_IN, inTransform);
+                mIpSecManager.applyTunnelModeTransform(
+                        mTunnelIface, IpSecManager.DIRECTION_OUT, outTransform);
+            } catch (IOException e) {
+                Log.d(TAG, "Transform application failed for token " + token, e);
+                onSessionLost(token, e);
             }
         }
 
         /**
          * Called when a new default network is connected.
          *
-         * <p>The Ikev2VpnRunner will unconditionally switch to the new network, killing the old IKE
-         * state in the process, and starting a new IkeSession instance.
+         * <p>The Ikev2VpnRunner will unconditionally switch to the new network. If the IKE session
+         * has mobility, Ikev2VpnRunner will migrate the existing IkeSession to the new network.
+         * Otherwise, Ikev2VpnRunner will kill the old IKE state, and start a new IkeSession
+         * instance.
          *
          * <p>This method MUST always be called on the mExecutor thread in order to ensure
          * consistency of the Ikev2VpnRunner fields.
          */
         public void onDefaultNetworkChanged(@NonNull Network network) {
-            Log.d(TAG, "Starting IKEv2/IPsec session on new network: " + network);
+            Log.d(TAG, "onDefaultNetworkChanged: " + network);
+
+            // If there is a new default network brought up, cancel the retry task to prevent
+            // establishing an unnecessary IKE session.
+            cancelRetryNewIkeSessionFuture();
+
+            // If there is a new default network brought up, cancel the obsolete reset and retry
+            // task.
+            cancelHandleNetworkLostTimeout();
+
+            if (!mIsRunning) {
+                Log.d(TAG, "onDefaultNetworkChanged after exit");
+                return; // VPN has been shut down.
+            }
+
+            mActiveNetwork = network;
+            mRetryCount = 0;
+
+            startOrMigrateIkeSession(network);
+        }
+
+        /**
+         * Start a new IKE session.
+         *
+         * <p>This method MUST always be called on the mExecutor thread in order to ensure
+         * consistency of the Ikev2VpnRunner fields.
+         *
+         * @param underlyingNetwork if the value is {@code null}, which means there is no active
+         *              network can be used, do nothing and return immediately. Otherwise, use the
+         *              given network to start a new IKE session.
+         */
+        private void startOrMigrateIkeSession(@Nullable Network underlyingNetwork) {
+            if (underlyingNetwork == null) {
+                Log.d(TAG, "There is no active network for starting an IKE session");
+                return;
+            }
 
             try {
-                if (!mIsRunning) {
-                    Log.d(TAG, "onDefaultNetworkChanged after exit");
-                    return; // VPN has been shut down.
+                if (mSession != null && mMobikeEnabled) {
+                    // IKE session can schedule a migration event only when IKE AUTH is finished
+                    // and mMobikeEnabled is true.
+                    Log.d(
+                            TAG,
+                            "Migrate IKE Session with token "
+                                    + mCurrentToken
+                                    + " to network "
+                                    + underlyingNetwork);
+                    mSession.setNetwork(underlyingNetwork);
+                    return;
                 }
 
+                Log.d(TAG, "Start new IKE session on network " + underlyingNetwork);
+
                 // Clear mInterface to prevent Ikev2VpnRunner being cleared when
                 // interfaceRemoved() is called.
                 mInterface = null;
                 // Without MOBIKE, we have no way to seamlessly migrate. Close on old
                 // (non-default) network, and start the new one.
                 resetIkeState();
-                mActiveNetwork = network;
 
                 // Get Ike options from IkeTunnelConnectionParams if it's available in the
                 // profile.
@@ -2854,12 +3067,12 @@
                 final ChildSessionParams childSessionParams;
                 if (ikeTunConnParams != null) {
                     final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(
-                            ikeTunConnParams.getIkeSessionParams()).setNetwork(network);
+                            ikeTunConnParams.getIkeSessionParams()).setNetwork(underlyingNetwork);
                     ikeSessionParams = builder.build();
                     childSessionParams = ikeTunConnParams.getTunnelModeChildSessionParams();
                 } else {
                     ikeSessionParams = VpnIkev2Utils.buildIkeSessionParams(
-                            mContext, mProfile, network);
+                            mContext, mProfile, underlyingNetwork);
                     childSessionParams = VpnIkev2Utils.buildChildSessionParams(
                             mProfile.getAllowedAlgorithms());
                 }
@@ -2867,29 +3080,50 @@
                 // TODO: Remove the need for adding two unused addresses with
                 // IPsec tunnels.
                 final InetAddress address = InetAddress.getLocalHost();
+
+                // When onChildOpened is called and transforms are applied, it is
+                // guaranteed that the underlying network is still "network", because the
+                // all the network switch events will be deferred before onChildOpened is
+                // called. Thus it is safe to build a mTunnelIface before IKE setup.
                 mTunnelIface =
                         mIpSecManager.createIpSecTunnelInterface(
-                                address /* unused */,
-                                address /* unused */,
-                                network);
+                                address /* unused */, address /* unused */, underlyingNetwork);
                 NetdUtils.setInterfaceUp(mNetd, mTunnelIface.getInterfaceName());
 
-                mSession = mIkev2SessionCreator.createIkeSession(
-                        mContext,
-                        ikeSessionParams,
-                        childSessionParams,
-                        mExecutor,
-                        new VpnIkev2Utils.IkeSessionCallbackImpl(
-                                TAG, IkeV2VpnRunner.this, network),
-                        new VpnIkev2Utils.ChildSessionCallbackImpl(
-                                TAG, IkeV2VpnRunner.this, network));
-                Log.d(TAG, "Ike Session started for network " + network);
+                final int token = ++mCurrentToken;
+                mSession =
+                        mIkev2SessionCreator.createIkeSession(
+                                mContext,
+                                ikeSessionParams,
+                                childSessionParams,
+                                mExecutor,
+                                new VpnIkev2Utils.IkeSessionCallbackImpl(
+                                        TAG, IkeV2VpnRunner.this, token),
+                                new VpnIkev2Utils.ChildSessionCallbackImpl(
+                                        TAG, IkeV2VpnRunner.this, token));
+                Log.d(TAG, "IKE session started for token " + token);
             } catch (Exception e) {
-                Log.i(TAG, "Setup failed for network " + network + ". Aborting", e);
-                onSessionLost(network, e);
+                Log.i(TAG, "Setup failed for token " + mCurrentToken + ". Aborting", e);
+                onSessionLost(mCurrentToken, e);
             }
         }
 
+        private void scheduleRetryNewIkeSession() {
+            final long retryDelay = mDeps.getNextRetryDelaySeconds(mRetryCount++);
+            Log.d(TAG, "Retry new IKE session after " + retryDelay + " seconds.");
+            // If the default network is lost during the retry delay, the mActiveNetwork will be
+            // null, and the new IKE session won't be established until there is a new default
+            // network bringing up.
+            mScheduledHandleRetryIkeSessionTimeout =
+                    mExecutor.schedule(() -> {
+                        startOrMigrateIkeSession(mActiveNetwork);
+
+                        // Reset mScheduledHandleRetryIkeSessionTimeout since it's already run on
+                        // executor thread.
+                        mScheduledHandleRetryIkeSessionTimeout = null;
+                    }, retryDelay, TimeUnit.SECONDS);
+        }
+
         /** Called when the NetworkCapabilities of underlying network is changed */
         public void onDefaultNetworkCapabilitiesChanged(@NonNull NetworkCapabilities nc) {
             mUnderlyingNetworkCapabilities = nc;
@@ -2900,6 +3134,99 @@
             mUnderlyingLinkProperties = lp;
         }
 
+        /**
+         * Handles loss of the default underlying network
+         *
+         * <p>If the IKE Session has mobility, Ikev2VpnRunner will schedule a teardown event with a
+         * delay so that the IKE Session can migrate if a new network is available soon. Otherwise,
+         * Ikev2VpnRunner will kill the IKE session and reset the VPN.
+         *
+         * <p>This method MUST always be called on the mExecutor thread in order to ensure
+         * consistency of the Ikev2VpnRunner fields.
+         */
+        public void onDefaultNetworkLost(@NonNull Network network) {
+            // If the default network is torn down, there is no need to call
+            // startOrMigrateIkeSession() since it will always check if there is an active network
+            // can be used or not.
+            cancelRetryNewIkeSessionFuture();
+
+            if (!isActiveNetwork(network)) {
+                Log.d(TAG, "onDefaultNetworkLost called for obsolete network " + network);
+
+                // Do nothing; this signals that either: (1) a new/better Network was found,
+                // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in
+                // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited,
+                // or an error was encountered somewhere else). In both cases, all resources and
+                // sessions are torn down via resetIkeState().
+                return;
+            } else {
+                mActiveNetwork = null;
+            }
+
+            if (mScheduledHandleNetworkLostTimeout != null
+                    && !mScheduledHandleNetworkLostTimeout.isCancelled()
+                    && !mScheduledHandleNetworkLostTimeout.isDone()) {
+                final IllegalStateException exception =
+                        new IllegalStateException(
+                                "Found a pending mScheduledHandleNetworkLostTimeout");
+                Log.i(
+                        TAG,
+                        "Unexpected error in onDefaultNetworkLost. Tear down session",
+                        exception);
+                handleSessionLost(exception, network);
+                return;
+            }
+
+            if (mSession != null && mMobikeEnabled) {
+                Log.d(
+                        TAG,
+                        "IKE Session has mobility. Delay handleSessionLost for losing network "
+                                + network
+                                + " on session with token "
+                                + mCurrentToken);
+
+                // Delay the teardown in case a new network will be available soon. For example,
+                // during handover between two WiFi networks, Android will disconnect from the
+                // first WiFi and then connects to the second WiFi.
+                mScheduledHandleNetworkLostTimeout =
+                        mExecutor.schedule(
+                                () -> {
+                                    handleSessionLost(null, network);
+                                },
+                                NETWORK_LOST_TIMEOUT_MS,
+                                TimeUnit.MILLISECONDS);
+            } else {
+                Log.d(TAG, "Call handleSessionLost for losing network " + network);
+                handleSessionLost(null, network);
+            }
+        }
+
+        private void cancelHandleNetworkLostTimeout() {
+            if (mScheduledHandleNetworkLostTimeout != null
+                    && !mScheduledHandleNetworkLostTimeout.isDone()) {
+                // It does not matter what to put in #cancel(boolean), because it is impossible
+                // that the task tracked by mScheduledHandleNetworkLostTimeout is
+                // in-progress since both that task and onDefaultNetworkChanged are submitted to
+                // mExecutor who has only one thread.
+                Log.d(TAG, "Cancel the task for handling network lost timeout");
+                mScheduledHandleNetworkLostTimeout.cancel(false /* mayInterruptIfRunning */);
+                mScheduledHandleNetworkLostTimeout = null;
+            }
+        }
+
+        private void cancelRetryNewIkeSessionFuture() {
+            if (mScheduledHandleRetryIkeSessionTimeout != null
+                    && !mScheduledHandleRetryIkeSessionTimeout.isDone()) {
+                // It does not matter what to put in #cancel(boolean), because it is impossible
+                // that the task tracked by mScheduledHandleRetryIkeSessionTimeout is
+                // in-progress since both that task and onDefaultNetworkChanged are submitted to
+                // mExecutor who has only one thread.
+                Log.d(TAG, "Cancel the task for handling new ike session timeout");
+                mScheduledHandleRetryIkeSessionTimeout.cancel(false /* mayInterruptIfRunning */);
+                mScheduledHandleRetryIkeSessionTimeout = null;
+            }
+        }
+
         /** Marks the state as FAILED, and disconnects. */
         private void markFailedAndDisconnect(Exception exception) {
             synchronized (Vpn.this) {
@@ -2918,18 +3245,28 @@
          * <p>This method MUST always be called on the mExecutor thread in order to ensure
          * consistency of the Ikev2VpnRunner fields.
          */
-        public void onSessionLost(@NonNull Network network, @Nullable Exception exception) {
-            if (!isActiveNetwork(network)) {
-                Log.d(TAG, "onSessionLost() called for obsolete network " + network);
+        public void onSessionLost(int token, @Nullable Exception exception) {
+            Log.d(TAG, "onSessionLost() called for token " + token);
+
+            if (!isActiveToken(token)) {
+                Log.d(TAG, "onSessionLost() called for obsolete token " + token);
 
                 // Do nothing; this signals that either: (1) a new/better Network was found,
-                // and the Ikev2VpnRunner has switched to it in onDefaultNetworkChanged, or (2) this
-                // IKE session was already shut down (exited, or an error was encountered somewhere
-                // else). In both cases, all resources and sessions are torn down via
-                // onSessionLost() and resetIkeState().
+                // and the Ikev2VpnRunner has switched to it by restarting a new IKE session in
+                // onDefaultNetworkChanged, or (2) this IKE session was already shut down (exited,
+                // or an error was encountered somewhere else). In both cases, all resources and
+                // sessions are torn down via resetIkeState().
                 return;
             }
 
+            handleSessionLost(exception, mActiveNetwork);
+        }
+
+        private void handleSessionLost(@Nullable Exception exception, @Nullable Network network) {
+            // Cancel mScheduledHandleNetworkLostTimeout if the session it is going to terminate is
+            // already terminated due to other failures.
+            cancelHandleNetworkLostTimeout();
+
             synchronized (Vpn.this) {
                 if (exception instanceof IkeProtocolException) {
                     final IkeProtocolException ikeException = (IkeProtocolException) exception;
@@ -2949,7 +3286,7 @@
                                         VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
                                         ikeException.getErrorType(),
                                         getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                        mActiveNetwork,
+                                        network,
                                         getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                                 mUnderlyingNetworkCapabilities),
                                         getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -2967,7 +3304,7 @@
                                         VpnManager.ERROR_CLASS_RECOVERABLE,
                                         ikeException.getErrorType(),
                                         getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                        mActiveNetwork,
+                                        network,
                                         getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                                 mUnderlyingNetworkCapabilities),
                                         getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -2986,7 +3323,7 @@
                                 VpnManager.ERROR_CLASS_RECOVERABLE,
                                 VpnManager.ERROR_CODE_NETWORK_LOST,
                                 getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                mActiveNetwork,
+                                network,
                                 getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                         mUnderlyingNetworkCapabilities),
                                 getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -3001,7 +3338,7 @@
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST,
                                     getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                    mActiveNetwork,
+                                    network,
                                     getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                             mUnderlyingNetworkCapabilities),
                                     getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -3015,7 +3352,7 @@
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT,
                                     getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                    mActiveNetwork,
+                                    network,
                                     getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                             mUnderlyingNetworkCapabilities),
                                     getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -3029,7 +3366,7 @@
                                     VpnManager.ERROR_CLASS_RECOVERABLE,
                                     VpnManager.ERROR_CODE_NETWORK_IO,
                                     getPackage(), mSessionKey, makeVpnProfileStateLocked(),
-                                    mActiveNetwork,
+                                    network,
                                     getRedactedNetworkCapabilitiesOfUnderlyingNetwork(
                                             mUnderlyingNetworkCapabilities),
                                     getRedactedLinkPropertiesOfUnderlyingNetwork(
@@ -3039,15 +3376,16 @@
                 } else if (exception != null) {
                     Log.wtf(TAG, "onSessionLost: exception = " + exception);
                 }
+
+                scheduleRetryNewIkeSession();
             }
 
-            mActiveNetwork = null;
             mUnderlyingNetworkCapabilities = null;
             mUnderlyingLinkProperties = null;
 
             // Close all obsolete state, but keep VPN alive incase a usable network comes up.
             // (Mirrors VpnService behavior)
-            Log.d(TAG, "Resetting state for network: " + network);
+            Log.d(TAG, "Resetting state for token: " + mCurrentToken);
 
             synchronized (Vpn.this) {
                 // Since this method handles non-fatal errors only, set mInterface to null to
@@ -3092,6 +3430,8 @@
                 mSession.kill(); // Kill here to make sure all resources are released immediately
                 mSession = null;
             }
+            mIkeConnectionInfo = null;
+            mMobikeEnabled = false;
         }
 
         /**
diff --git a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
index 1705828..857c86d 100644
--- a/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
+++ b/services/core/java/com/android/server/connectivity/VpnIkev2Utils.java
@@ -68,6 +68,7 @@
 import android.net.ipsec.ike.IkeSaProposal;
 import android.net.ipsec.ike.IkeSessionCallback;
 import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
 import android.net.ipsec.ike.IkeSessionParams;
 import android.net.ipsec.ike.IkeTrafficSelector;
 import android.net.ipsec.ike.TunnelModeChildSessionParams;
@@ -107,6 +108,7 @@
                 new IkeSessionParams.Builder(context)
                         .setServerHostname(profile.getServerAddr())
                         .setNetwork(network)
+                        .addIkeOption(IkeSessionParams.IKE_OPTION_MOBIKE)
                         .setLocalIdentification(localId)
                         .setRemoteIdentification(remoteId);
         setIkeAuth(profile, ikeOptionsBuilder);
@@ -298,72 +300,79 @@
     static class IkeSessionCallbackImpl implements IkeSessionCallback {
         private final String mTag;
         private final Vpn.IkeV2VpnRunnerCallback mCallback;
-        private final Network mNetwork;
+        private final int mToken;
 
-        IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) {
+        IkeSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) {
             mTag = tag;
             mCallback = callback;
-            mNetwork = network;
+            mToken = token;
         }
 
         @Override
         public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) {
-            Log.d(mTag, "IkeOpened for network " + mNetwork);
-            // Nothing to do here.
+            Log.d(mTag, "IkeOpened for token " + mToken);
+            mCallback.onIkeOpened(mToken, ikeSessionConfig);
         }
 
         @Override
         public void onClosed() {
-            Log.d(mTag, "IkeClosed for network " + mNetwork);
-            mCallback.onSessionLost(mNetwork, null); // Server requested session closure. Retry?
+            Log.d(mTag, "IkeClosed for token " + mToken);
+            mCallback.onSessionLost(mToken, null); // Server requested session closure. Retry?
         }
 
         @Override
         public void onClosedExceptionally(@NonNull IkeException exception) {
-            Log.d(mTag, "IkeClosedExceptionally for network " + mNetwork, exception);
-            mCallback.onSessionLost(mNetwork, exception);
+            Log.d(mTag, "IkeClosedExceptionally for token " + mToken, exception);
+            mCallback.onSessionLost(mToken, exception);
         }
 
         @Override
         public void onError(@NonNull IkeProtocolException exception) {
-            Log.d(mTag, "IkeError for network " + mNetwork, exception);
+            Log.d(mTag, "IkeError for token " + mToken, exception);
             // Non-fatal, log and continue.
         }
+
+        @Override
+        public void onIkeSessionConnectionInfoChanged(
+                @NonNull IkeSessionConnectionInfo connectionInfo) {
+            Log.d(mTag, "onIkeSessionConnectionInfoChanged for token " + mToken);
+            mCallback.onIkeConnectionInfoChanged(mToken, connectionInfo);
+        }
     }
 
     static class ChildSessionCallbackImpl implements ChildSessionCallback {
         private final String mTag;
         private final Vpn.IkeV2VpnRunnerCallback mCallback;
-        private final Network mNetwork;
+        private final int mToken;
 
-        ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, Network network) {
+        ChildSessionCallbackImpl(String tag, Vpn.IkeV2VpnRunnerCallback callback, int token) {
             mTag = tag;
             mCallback = callback;
-            mNetwork = network;
+            mToken = token;
         }
 
         @Override
         public void onOpened(@NonNull ChildSessionConfiguration childConfig) {
-            Log.d(mTag, "ChildOpened for network " + mNetwork);
-            mCallback.onChildOpened(mNetwork, childConfig);
+            Log.d(mTag, "ChildOpened for token " + mToken);
+            mCallback.onChildOpened(mToken, childConfig);
         }
 
         @Override
         public void onClosed() {
-            Log.d(mTag, "ChildClosed for network " + mNetwork);
-            mCallback.onSessionLost(mNetwork, null);
+            Log.d(mTag, "ChildClosed for token " + mToken);
+            mCallback.onSessionLost(mToken, null);
         }
 
         @Override
         public void onClosedExceptionally(@NonNull IkeException exception) {
-            Log.d(mTag, "ChildClosedExceptionally for network " + mNetwork, exception);
-            mCallback.onSessionLost(mNetwork, exception);
+            Log.d(mTag, "ChildClosedExceptionally for token " + mToken, exception);
+            mCallback.onSessionLost(mToken, exception);
         }
 
         @Override
         public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) {
-            Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; network " + mNetwork);
-            mCallback.onChildTransformCreated(mNetwork, transform, direction);
+            Log.d(mTag, "ChildTransformCreated; Direction: " + direction + "; token " + mToken);
+            mCallback.onChildTransformCreated(mToken, transform, direction);
         }
 
         @Override
@@ -371,8 +380,15 @@
             // Nothing to be done; no references to the IpSecTransform are held by the
             // Ikev2VpnRunner (or this callback class), and this transform will be closed by the
             // IKE library.
-            Log.d(mTag,
-                    "ChildTransformDeleted; Direction: " + direction + "; for network " + mNetwork);
+            Log.d(mTag, "ChildTransformDeleted; Direction: " + direction + "; for token " + mToken);
+        }
+
+        @Override
+        public void onIpSecTransformsMigrated(
+                @NonNull IpSecTransform inIpSecTransform,
+                @NonNull IpSecTransform outIpSecTransform) {
+            Log.d(mTag, "ChildTransformsMigrated; token " + mToken);
+            mCallback.onChildMigrated(mToken, inIpSecTransform, outIpSecTransform);
         }
     }
 
@@ -390,7 +406,7 @@
 
         @Override
         public void onAvailable(@NonNull Network network) {
-            Log.d(mTag, "Starting IKEv2/IPsec session on new network: " + network);
+            Log.d(mTag, "onAvailable called for network: " + network);
             mExecutor.execute(() -> mCallback.onDefaultNetworkChanged(network));
         }
 
@@ -412,8 +428,8 @@
 
         @Override
         public void onLost(@NonNull Network network) {
-            Log.d(mTag, "Tearing down; lost network: " + network);
-            mExecutor.execute(() -> mCallback.onSessionLost(network, null));
+            Log.d(mTag, "onLost called for network: " + network);
+            mExecutor.execute(() -> mCallback.onDefaultNetworkLost(network));
         }
     }
 
diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java
index 767b2d1..eccee52 100644
--- a/services/core/java/com/android/server/display/BrightnessThrottler.java
+++ b/services/core/java/com/android/server/display/BrightnessThrottler.java
@@ -16,21 +16,31 @@
 
 package com.android.server.display;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IThermalEventListener;
 import android.os.IThermalService;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Temperature;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfigInterface;
 import android.util.Slog;
 
-import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData;
+import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * This class monitors various conditions, such as skin temperature throttling status, and limits
@@ -44,28 +54,54 @@
 
     private final Injector mInjector;
     private final Handler mHandler;
-    private BrightnessThrottlingData mThrottlingData;
+    // We need a separate handler for unit testing. These two handlers are the same throughout the
+    // non-test code.
+    private final Handler mDeviceConfigHandler;
     private final Runnable mThrottlingChangeCallback;
     private final SkinThermalStatusObserver mSkinThermalStatusObserver;
+    private final DeviceConfigListener mDeviceConfigListener;
+    private final DeviceConfigInterface mDeviceConfig;
+
     private int mThrottlingStatus;
+    private BrightnessThrottlingData mThrottlingData;
+    private BrightnessThrottlingData mDdcThrottlingData;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
     private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason =
         BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
+    private String mUniqueDisplayId;
+
+    // The most recent string that has been set from DeviceConfig
+    private String mBrightnessThrottlingDataString;
+
+    // This is a collection of brightness throttling data that has been written as overrides from
+    // the DeviceConfig. This will always take priority over the display device config data.
+    private HashMap<String, BrightnessThrottlingData> mBrightnessThrottlingDataOverride =
+            new HashMap<>(1);
 
     BrightnessThrottler(Handler handler, BrightnessThrottlingData throttlingData,
-            Runnable throttlingChangeCallback) {
-        this(new Injector(), handler, throttlingData, throttlingChangeCallback);
+            Runnable throttlingChangeCallback, String uniqueDisplayId) {
+        this(new Injector(), handler, handler, throttlingData, throttlingChangeCallback,
+                uniqueDisplayId);
     }
 
-    BrightnessThrottler(Injector injector, Handler handler, BrightnessThrottlingData throttlingData,
-            Runnable throttlingChangeCallback) {
+    @VisibleForTesting
+    BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler,
+            BrightnessThrottlingData throttlingData, Runnable throttlingChangeCallback,
+            String uniqueDisplayId) {
         mInjector = injector;
+
         mHandler = handler;
+        mDeviceConfigHandler = deviceConfigHandler;
         mThrottlingData = throttlingData;
+        mDdcThrottlingData = throttlingData;
         mThrottlingChangeCallback = throttlingChangeCallback;
         mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
 
-        resetThrottlingData(mThrottlingData);
+        mUniqueDisplayId = uniqueDisplayId;
+        mDeviceConfig = injector.getDeviceConfig();
+        mDeviceConfigListener = new DeviceConfigListener();
+
+        resetThrottlingData(mThrottlingData, mUniqueDisplayId);
     }
 
     boolean deviceSupportsThrottling() {
@@ -86,7 +122,7 @@
 
     void stop() {
         mSkinThermalStatusObserver.stopObserving();
-
+        mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener);
         // We're asked to stop throttling, so reset brightness restrictions.
         mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
         mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
@@ -97,9 +133,19 @@
         mThrottlingStatus = THROTTLING_INVALID;
     }
 
-    void resetThrottlingData(BrightnessThrottlingData throttlingData) {
+    private void resetThrottlingData() {
+        resetThrottlingData(mDdcThrottlingData, mUniqueDisplayId);
+    }
+
+    void resetThrottlingData(BrightnessThrottlingData throttlingData, String displayId) {
         stop();
-        mThrottlingData = throttlingData;
+
+        mUniqueDisplayId = displayId;
+        mDdcThrottlingData = throttlingData;
+        mDeviceConfigListener.startListening();
+        reloadBrightnessThrottlingDataOverride();
+        mThrottlingData = mBrightnessThrottlingDataOverride.getOrDefault(mUniqueDisplayId,
+                throttlingData);
 
         if (deviceSupportsThrottling()) {
             mSkinThermalStatusObserver.startObserving();
@@ -173,14 +219,148 @@
     private void dumpLocal(PrintWriter pw) {
         pw.println("BrightnessThrottler:");
         pw.println("  mThrottlingData=" + mThrottlingData);
+        pw.println("  mDdcThrottlingData=" + mDdcThrottlingData);
+        pw.println("  mUniqueDisplayId=" + mUniqueDisplayId);
         pw.println("  mThrottlingStatus=" + mThrottlingStatus);
         pw.println("  mBrightnessCap=" + mBrightnessCap);
         pw.println("  mBrightnessMaxReason=" +
             BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason));
+        pw.println("  mBrightnessThrottlingDataOverride=" + mBrightnessThrottlingDataOverride);
+        pw.println("  mBrightnessThrottlingDataString=" + mBrightnessThrottlingDataString);
 
         mSkinThermalStatusObserver.dump(pw);
     }
 
+    private String getBrightnessThrottlingDataString() {
+        return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA,
+                /* defaultValue= */ null);
+    }
+
+    private boolean parseAndSaveData(@NonNull String strArray,
+            @NonNull HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData) {
+        boolean validConfig = true;
+        String[] items = strArray.split(",");
+        int i = 0;
+
+        try {
+            String uniqueDisplayId = items[i++];
+
+            // number of throttling points
+            int noOfThrottlingPoints = Integer.parseInt(items[i++]);
+            List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints);
+
+            // throttling level and point
+            for (int j = 0; j < noOfThrottlingPoints; j++) {
+                String severity = items[i++];
+                int status = parseThermalStatus(severity);
+
+                float brightnessPoint = parseBrightness(items[i++]);
+
+                throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint));
+            }
+            BrightnessThrottlingData toSave =
+                    DisplayDeviceConfig.BrightnessThrottlingData.create(throttlingLevels);
+            tempBrightnessThrottlingData.put(uniqueDisplayId, toSave);
+        } catch (NumberFormatException | IndexOutOfBoundsException
+                | UnknownThermalStatusException e) {
+            validConfig = false;
+            Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e);
+        }
+
+        if (i != items.length) {
+            validConfig = false;
+        }
+
+        return validConfig;
+    }
+
+    public void reloadBrightnessThrottlingDataOverride() {
+        HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData =
+                new HashMap<>(1);
+        mBrightnessThrottlingDataString = getBrightnessThrottlingDataString();
+        boolean validConfig = true;
+        mBrightnessThrottlingDataOverride.clear();
+        if (mBrightnessThrottlingDataString != null) {
+            String[] throttlingDataSplits = mBrightnessThrottlingDataString.split(";");
+            for (String s : throttlingDataSplits) {
+                if (!parseAndSaveData(s, tempBrightnessThrottlingData)) {
+                    validConfig = false;
+                    break;
+                }
+            }
+
+            if (validConfig) {
+                mBrightnessThrottlingDataOverride.putAll(tempBrightnessThrottlingData);
+                tempBrightnessThrottlingData.clear();
+            }
+
+        } else {
+            Slog.w(TAG, "DeviceConfig BrightnessThrottlingData is null");
+        }
+    }
+
+    /**
+     * Listens to config data change and updates the brightness throttling data using
+     * DisplayManager#KEY_BRIGHTNESS_THROTTLING_DATA.
+     * The format should be a string similar to: "local:4619827677550801152,2,moderate,0.5,severe,
+     * 0.379518072;local:4619827677550801151,1,moderate,0.75"
+     * In this order:
+     * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>]
+     * Where the latter part is repeated for each throttling level, and the entirety is repeated
+     * for each display, separated by a semicolon.
+     */
+    public class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+        public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler);
+
+        public void startListening() {
+            mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    mExecutor, this);
+        }
+
+        @Override
+        public void onPropertiesChanged(DeviceConfig.Properties properties) {
+            reloadBrightnessThrottlingDataOverride();
+            resetThrottlingData();
+        }
+    }
+
+    private float parseBrightness(String intVal) throws NumberFormatException {
+        float value = Float.parseFloat(intVal);
+        if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) {
+            throw new NumberFormatException("Brightness constraint value out of bounds.");
+        }
+        return value;
+    }
+
+    @PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value)
+            throws UnknownThermalStatusException {
+        switch (value) {
+            case "none":
+                return PowerManager.THERMAL_STATUS_NONE;
+            case "light":
+                return PowerManager.THERMAL_STATUS_LIGHT;
+            case "moderate":
+                return PowerManager.THERMAL_STATUS_MODERATE;
+            case "severe":
+                return PowerManager.THERMAL_STATUS_SEVERE;
+            case "critical":
+                return PowerManager.THERMAL_STATUS_CRITICAL;
+            case "emergency":
+                return PowerManager.THERMAL_STATUS_EMERGENCY;
+            case "shutdown":
+                return PowerManager.THERMAL_STATUS_SHUTDOWN;
+            default:
+                throw new UnknownThermalStatusException("Invalid Thermal Status: " + value);
+        }
+    }
+
+    private static class UnknownThermalStatusException extends Exception {
+        UnknownThermalStatusException(String message) {
+            super(message);
+        }
+    }
+
     private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
         private final Injector mInjector;
         private final Handler mHandler;
@@ -258,5 +438,10 @@
             return IThermalService.Stub.asInterface(
                     ServiceManager.getService(Context.THERMAL_SERVICE));
         }
+
+        @NonNull
+        public DeviceConfigInterface getDeviceConfig() {
+            return DeviceConfigInterface.REAL;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index a25ac21..2322280d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -279,10 +279,14 @@
     private HighBrightnessModeData mHbmData;
     private DensityMapping mDensityMapping;
     private String mLoadedFrom = null;
-
-    private BrightnessThrottlingData mBrightnessThrottlingData;
     private Spline mSdrToHdrRatioSpline;
 
+    // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original
+    // data, which comes from the ddc, and the current one, which may be the DeviceConfig
+    // overwritten value.
+    private BrightnessThrottlingData mBrightnessThrottlingData;
+    private BrightnessThrottlingData mOriginalBrightnessThrottlingData;
+
     private DisplayDeviceConfig(Context context) {
         mContext = context;
     }
@@ -422,6 +426,10 @@
         return config;
     }
 
+    void setBrightnessThrottlingData(BrightnessThrottlingData brightnessThrottlingData) {
+        mBrightnessThrottlingData = brightnessThrottlingData;
+    }
+
     /**
      * Return the brightness mapping nits array.
      *
@@ -637,6 +645,7 @@
                 + ", mHbmData=" + mHbmData
                 + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
                 + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
+                + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
                 + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
                 + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
                 + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease
@@ -932,6 +941,7 @@
 
         if (!badConfig) {
             mBrightnessThrottlingData = BrightnessThrottlingData.create(throttlingLevels);
+            mOriginalBrightnessThrottlingData = mBrightnessThrottlingData;
         }
     }
 
@@ -1407,7 +1417,9 @@
     /**
      * Container for brightness throttling data.
      */
-    static class BrightnessThrottlingData {
+    public static class BrightnessThrottlingData {
+        public List<ThrottlingLevel> throttlingLevels;
+
         static class ThrottlingLevel {
             public @PowerManager.ThermalStatus int thermalStatus;
             public float brightness;
@@ -1421,9 +1433,25 @@
             public String toString() {
                 return "[" + thermalStatus + "," + brightness + "]";
             }
-        }
 
-        public List<ThrottlingLevel> throttlingLevels;
+            @Override
+            public boolean equals(Object obj) {
+                if (!(obj instanceof ThrottlingLevel)) {
+                    return false;
+                }
+                ThrottlingLevel otherThrottlingLevel = (ThrottlingLevel) obj;
+
+                return otherThrottlingLevel.thermalStatus == this.thermalStatus
+                        && otherThrottlingLevel.brightness == this.brightness;
+            }
+            @Override
+            public int hashCode() {
+                int result = 1;
+                result = 31 * result + thermalStatus;
+                result = 31 * result + Float.hashCode(brightness);
+                return result;
+            }
+        }
 
         static public BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels)
         {
@@ -1482,12 +1510,30 @@
                 + "} ";
         }
 
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (!(obj instanceof BrightnessThrottlingData)) {
+                return false;
+            }
+
+            BrightnessThrottlingData otherBrightnessThrottlingData = (BrightnessThrottlingData) obj;
+            return throttlingLevels.equals(otherBrightnessThrottlingData.throttlingLevels);
+        }
+
+        @Override
+        public int hashCode() {
+            return throttlingLevels.hashCode();
+        }
+
         private BrightnessThrottlingData(List<ThrottlingLevel> inLevels) {
             throttlingLevels = new ArrayList<>(inLevels.size());
             for (ThrottlingLevel level : inLevels) {
                 throttlingLevels.add(new ThrottlingLevel(level.thermalStatus, level.brightness));
             }
         }
-
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d05a902..95c8fef 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -461,6 +461,18 @@
 
     private boolean mIsRbcActive;
 
+    // Whether there's a callback to tell listeners the display has changed scheduled to run. When
+    // true it implies a wakelock is being held to guarantee the update happens before we collapse
+    // into suspend and so needs to be cleaned up if the thread is exiting.
+    // Should only be accessed on the Handler thread.
+    private boolean mOnStateChangedPending;
+
+    // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many
+    // suspend blocker acquisitions are pending when shutting down this DPC.
+    // Should only be accessed on the Handler thread.
+    private int mOnProximityPositiveMessages;
+    private int mOnProximityNegativeMessages;
+
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
     private ObjectAnimator mColorFadeOffAnimator;
@@ -861,7 +873,7 @@
                     }
                 });
         mBrightnessThrottler.resetThrottlingData(
-                mDisplayDeviceConfig.getBrightnessThrottlingData());
+                mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId);
     }
 
     private void sendUpdatePowerState() {
@@ -1091,10 +1103,24 @@
         mHbmController.stop();
         mBrightnessThrottler.stop();
         mHandler.removeCallbacksAndMessages(null);
+
+        // Release any outstanding wakelocks we're still holding because of pending messages.
         if (mUnfinishedBusiness) {
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
             mUnfinishedBusiness = false;
         }
+        if (mOnStateChangedPending) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+            mOnStateChangedPending = false;
+        }
+        for (int i = 0; i < mOnProximityPositiveMessages; i++) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
+        }
+        mOnProximityPositiveMessages = 0;
+        for (int i = 0; i < mOnProximityNegativeMessages; i++) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
+        }
+        mOnProximityNegativeMessages = 0;
 
         final float brightness = mPowerState != null
             ? mPowerState.getScreenBrightness()
@@ -1816,7 +1842,7 @@
                 () -> {
                     sendUpdatePowerStateLocked();
                     postBrightnessChangeRunnable();
-                });
+                }, mUniqueDisplayId);
     }
 
     private void blockScreenOn() {
@@ -2248,8 +2274,11 @@
     }
 
     private void sendOnStateChangedWithWakelock() {
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-        mHandler.post(mOnStateChangedRunnable);
+        if (!mOnStateChangedPending) {
+            mOnStateChangedPending = true;
+            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+            mHandler.post(mOnStateChangedRunnable);
+        }
     }
 
     private void logDisplayPolicyChanged(int newPolicy) {
@@ -2408,6 +2437,7 @@
     private final Runnable mOnStateChangedRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnStateChangedPending = false;
             mCallbacks.onStateChanged();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
         }
@@ -2416,17 +2446,20 @@
     private void sendOnProximityPositiveWithWakelock() {
         mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
         mHandler.post(mOnProximityPositiveRunnable);
+        mOnProximityPositiveMessages++;
     }
 
     private final Runnable mOnProximityPositiveRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnProximityPositiveMessages--;
             mCallbacks.onProximityPositive();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
         }
     };
 
     private void sendOnProximityNegativeWithWakelock() {
+        mOnProximityNegativeMessages++;
         mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
         mHandler.post(mOnProximityNegativeRunnable);
     }
@@ -2434,6 +2467,7 @@
     private final Runnable mOnProximityNegativeRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnProximityNegativeMessages--;
             mCallbacks.onProximityNegative();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
         }
@@ -2533,6 +2567,9 @@
         pw.println("  mReportedToPolicy="
                 + reportedToPolicyToString(mReportedScreenStateToPolicy));
         pw.println("  mIsRbcActive=" + mIsRbcActive);
+        pw.println("  mOnStateChangePending=" + mOnStateChangedPending);
+        pw.println("  mOnProximityPositiveMessages=" + mOnProximityPositiveMessages);
+        pw.println("  mOnProximityNegativeMessages=" + mOnProximityNegativeMessages);
 
         if (mScreenBrightnessRampAnimator != null) {
             pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index 8de150a..223b8c1 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -956,6 +956,8 @@
                         R.array.config_availableColorModes);
                 if (availableColorModes.length > 0) {
                     colorMode = availableColorModes[0];
+                } else {
+                    colorMode = NOT_SET;
                 }
             }
         }
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 098e8f7..7d12ede 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -46,7 +46,6 @@
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.ContentRecordingSession;
-import android.window.WindowContainerToken;
 
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
@@ -433,7 +432,7 @@
         private IBinder mToken;
         private IBinder.DeathRecipient mDeathEater;
         private boolean mRestoreSystemAlertWindow;
-        private WindowContainerToken mTaskRecordingWindowContainerToken = null;
+        private IBinder mLaunchCookie = null;
 
         MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
                 boolean isPrivileged) {
@@ -609,14 +608,13 @@
         }
 
         @Override // Binder call
-        public void setTaskRecordingWindowContainerToken(WindowContainerToken token) {
-            // TODO(b/221417940) set the task id to record from sysui, for the package chosen.
-            mTaskRecordingWindowContainerToken = token;
+        public void setLaunchCookie(IBinder launchCookie) {
+            mLaunchCookie = launchCookie;
         }
 
         @Override // Binder call
-        public WindowContainerToken getTaskRecordingWindowContainerToken() {
-            return mTaskRecordingWindowContainerToken;
+        public IBinder getLaunchCookie() {
+            return mLaunchCookie;
         }
 
         public MediaProjectionInfo getProjectionInfo() {
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index de9102a..6135fe8 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -340,7 +340,8 @@
             int newRuleInstanceCount = getCurrentInstanceCount(automaticZenRule.getOwner())
                     + getCurrentInstanceCount(automaticZenRule.getConfigurationActivity())
                     + 1;
-            if (newRuleInstanceCount > RULE_LIMIT_PER_PACKAGE
+            int newPackageRuleCount = getPackageRuleCount(pkg) + 1;
+            if (newPackageRuleCount > RULE_LIMIT_PER_PACKAGE
                     || (ruleInstanceLimit > 0 && ruleInstanceLimit < newRuleInstanceCount)) {
                 throw new IllegalArgumentException("Rule instance limit exceeded");
             }
@@ -521,6 +522,23 @@
         return count;
     }
 
+    // Equivalent method to getCurrentInstanceCount, but for all rules associated with a specific
+    // package rather than a condition provider service or activity.
+    private int getPackageRuleCount(String pkg) {
+        if (pkg == null) {
+            return 0;
+        }
+        int count = 0;
+        synchronized (mConfig) {
+            for (ZenRule rule : mConfig.automaticRules.values()) {
+                if (pkg.equals(rule.getPkg())) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
     public boolean canManageAutomaticZenRule(ZenRule rule) {
         final int callingUid = Binder.getCallingUid();
         if (callingUid == 0 || callingUid == Process.SYSTEM_UID) {
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index af0a20d..6cfe093 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -936,12 +936,15 @@
             String classLoaderContext, int profileAnalysisResult, boolean downgrade,
             int dexoptFlags, String oatDir) {
         final boolean shouldBePublic = (dexoptFlags & DEXOPT_PUBLIC) != 0;
-        // If the artifacts should be public while the current artifacts are not, we should
-        // re-compile anyway.
-        if (shouldBePublic && isOdexPrivate(packageName, path, isa, oatDir)) {
-            // Ensure compilation by pretending a compiler filter change on the apk/odex location
-            // (the reason for the '-'. A positive value means the 'oat' location).
-            return adjustDexoptNeeded(-DexFile.DEX2OAT_FOR_FILTER);
+        final boolean isProfileGuidedFilter = (dexoptFlags & DEXOPT_PROFILE_GUIDED) != 0;
+        boolean newProfile = profileAnalysisResult == PROFILE_ANALYSIS_OPTIMIZE;
+
+        if (!newProfile && isProfileGuidedFilter && shouldBePublic
+                && isOdexPrivate(packageName, path, isa, oatDir)) {
+            // The profile that will be used is a cloud profile, while the profile used previously
+            // is a user profile. Typically, this happens after an app starts being used by other
+            // apps.
+            newProfile = true;
         }
 
         int dexoptNeeded;
@@ -959,7 +962,6 @@
                     && profileAnalysisResult == PROFILE_ANALYSIS_DONT_OPTIMIZE_EMPTY_PROFILES) {
                 actualCompilerFilter = "verify";
             }
-            boolean newProfile = profileAnalysisResult == PROFILE_ANALYSIS_OPTIMIZE;
             dexoptNeeded = DexFile.getDexOptNeeded(path, isa, actualCompilerFilter,
                     classLoaderContext, newProfile, downgrade);
         } catch (IOException ioe) {
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index a01942d..bb23d89d 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -1346,7 +1346,7 @@
         private String getDeviceOwnerDeletedPackageMsg() {
             DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
             return dpm.getResources().getString(PACKAGE_DELETED_BY_DO,
-                    () -> mContext.getString(R.string.package_updated_device_owner));
+                    () -> mContext.getString(R.string.package_deleted_device_owner));
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
index 88b4a94..03e568c 100644
--- a/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/LegacyPermissionManagerService.java
@@ -413,8 +413,8 @@
                 return result;
             }
             mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(
-                    AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, uid, packageName,
-                    attributionTag, reason);
+                    AppOpsManager.OP_RECORD_AUDIO_HOTWORD, uid, packageName, attributionTag,
+                    reason);
             return result;
         }
     }
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index 06a54a4..9bfb40f 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -1540,6 +1540,7 @@
             try {
                 int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION;
                 String minCode = null;
+                boolean minAssigned = false;
                 int targetVers = ParsingUtils.DEFAULT_TARGET_SDK_VERSION;
                 String targetCode = null;
                 int maxVers = Integer.MAX_VALUE;
@@ -1548,9 +1549,11 @@
                 if (val != null) {
                     if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                         minCode = val.string.toString();
+                        minAssigned = !TextUtils.isEmpty(minCode);
                     } else {
                         // If it's not a string, it's an integer.
                         minVers = val.data;
+                        minAssigned = true;
                     }
                 }
 
@@ -1558,7 +1561,7 @@
                 if (val != null) {
                     if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                         targetCode = val.string.toString();
-                        if (minCode == null) {
+                        if (!minAssigned) {
                             minCode = targetCode;
                         }
                     } else {
diff --git a/services/core/java/com/android/server/testharness/TestHarnessModeService.java b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
index b6a4135..452bdf4 100644
--- a/services/core/java/com/android/server/testharness/TestHarnessModeService.java
+++ b/services/core/java/com/android/server/testharness/TestHarnessModeService.java
@@ -189,6 +189,7 @@
         if (adbManager.getAdbTempKeysFile() != null) {
             writeBytesToFile(persistentData.mAdbTempKeys, adbManager.getAdbTempKeysFile().toPath());
         }
+        adbManager.notifyKeyFilesUpdated();
     }
 
     private void configureUser() {
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 78b1c20..d79837b 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -61,12 +61,11 @@
         IGNORED_BACKGROUND,
         IGNORED_UNKNOWN_VIBRATION,
         IGNORED_UNSUPPORTED,
-        IGNORED_FOR_ALARM,
         IGNORED_FOR_EXTERNAL,
+        IGNORED_FOR_HIGHER_IMPORTANCE,
         IGNORED_FOR_ONGOING,
         IGNORED_FOR_POWER,
         IGNORED_FOR_RINGER_MODE,
-        IGNORED_FOR_RINGTONE,
         IGNORED_FOR_SETTINGS,
         IGNORED_SUPERSEDED,
     }
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index f0911ca..5ac2f4f 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -713,16 +713,17 @@
             case IGNORED_ERROR_APP_OPS:
                 Slog.w(TAG, "Would be an error: vibrate from uid " + uid);
                 break;
-            case IGNORED_FOR_ALARM:
-                if (DEBUG) {
-                    Slog.d(TAG, "Ignoring incoming vibration in favor of alarm vibration");
-                }
-                break;
             case IGNORED_FOR_EXTERNAL:
                 if (DEBUG) {
                     Slog.d(TAG, "Ignoring incoming vibration for current external vibration");
                 }
                 break;
+            case IGNORED_FOR_HIGHER_IMPORTANCE:
+                if (DEBUG) {
+                    Slog.d(TAG, "Ignoring incoming vibration in favor of ongoing vibration"
+                            + " with higher importance");
+                }
+                break;
             case IGNORED_FOR_ONGOING:
                 if (DEBUG) {
                     Slog.d(TAG, "Ignoring incoming vibration in favor of repeating vibration");
@@ -734,12 +735,6 @@
                             + attrs);
                 }
                 break;
-            case IGNORED_FOR_RINGTONE:
-                if (DEBUG) {
-                    Slog.d(TAG, "Ignoring incoming vibration in favor of ringtone vibration");
-                }
-                break;
-
             default:
                 if (DEBUG) {
                     Slog.d(TAG, "Vibration for uid=" + uid + " and with attrs=" + attrs
@@ -812,20 +807,43 @@
             return null;
         }
 
-        if (currentVibration.attrs.getUsage() == VibrationAttributes.USAGE_ALARM) {
-            return Vibration.Status.IGNORED_FOR_ALARM;
-        }
-
-        if (currentVibration.attrs.getUsage() == VibrationAttributes.USAGE_RINGTONE) {
-            return Vibration.Status.IGNORED_FOR_RINGTONE;
+        int currentUsage = currentVibration.attrs.getUsage();
+        int newUsage = vib.attrs.getUsage();
+        if (getVibrationImportance(currentUsage) > getVibrationImportance(newUsage)) {
+            // Current vibration has higher importance than this one and should not be cancelled.
+            return Vibration.Status.IGNORED_FOR_HIGHER_IMPORTANCE;
         }
 
         if (currentVibration.isRepeating()) {
+            // Current vibration is repeating, assume it's more important.
             return Vibration.Status.IGNORED_FOR_ONGOING;
         }
+
         return null;
     }
 
+    private static int getVibrationImportance(@VibrationAttributes.Usage int usage) {
+        switch (usage) {
+            case VibrationAttributes.USAGE_RINGTONE:
+                return 5;
+            case VibrationAttributes.USAGE_ALARM:
+                return 4;
+            case VibrationAttributes.USAGE_NOTIFICATION:
+                return 3;
+            case VibrationAttributes.USAGE_COMMUNICATION_REQUEST:
+            case VibrationAttributes.USAGE_ACCESSIBILITY:
+                return 2;
+            case VibrationAttributes.USAGE_HARDWARE_FEEDBACK:
+            case VibrationAttributes.USAGE_PHYSICAL_EMULATION:
+                return 1;
+            case VibrationAttributes.USAGE_MEDIA:
+            case VibrationAttributes.USAGE_TOUCH:
+            case VibrationAttributes.USAGE_UNKNOWN:
+            default:
+                return 0;
+        }
+    }
+
     /**
      * Check if given vibration should be ignored by this service.
      *
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index 622de57..270891f 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -56,6 +56,10 @@
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE;
 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
 import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
 import static com.android.internal.util.FrameworkStatsLog.CAMERA_COMPAT_CONTROL_EVENT_REPORTED__EVENT__APPEARED_APPLY_TREATMENT;
@@ -1376,7 +1380,7 @@
             return;
         }
 
-        logAppCompatStateInternal(activity, state, packageUid, compatStateInfo);
+        logAppCompatStateInternal(activity, state, compatStateInfo);
     }
 
     /**
@@ -1416,18 +1420,61 @@
             }
         }
         if (activityToLog != null && stateToLog != APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE) {
-            logAppCompatStateInternal(activityToLog, stateToLog, packageUid, compatStateInfo);
+            logAppCompatStateInternal(activityToLog, stateToLog, compatStateInfo);
         }
     }
 
+    private static boolean isAppCompateStateChangedToLetterboxed(int state) {
+        return state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO
+                || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION
+                || state == APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
+    }
+
     private void logAppCompatStateInternal(@NonNull ActivityRecord activity, int state,
-            int packageUid, PackageCompatStateInfo compatStateInfo) {
+             PackageCompatStateInfo compatStateInfo) {
         compatStateInfo.mLastLoggedState = state;
         compatStateInfo.mLastLoggedActivity = activity;
-        FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED, packageUid, state);
+        int packageUid = activity.info.applicationInfo.uid;
+
+        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__NOT_LETTERBOXED_POSITION;
+        if (isAppCompateStateChangedToLetterboxed(state)) {
+            positionToLog = activity.mLetterboxUiController.getLetterboxPositionForLogging();
+        }
+        FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPAT_STATE_CHANGED,
+                packageUid, state, positionToLog);
 
         if (DEBUG_METRICS) {
-            Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s)", packageUid, state));
+            Slog.i(TAG, String.format("APP_COMPAT_STATE_CHANGED(%s, %s, %s)",
+                    packageUid, state, positionToLog));
+        }
+    }
+
+    /**
+     * Logs the changing of the letterbox position along with its package UID
+     */
+    void logLetterboxPositionChange(@NonNull ActivityRecord activity, int position) {
+        int packageUid = activity.info.applicationInfo.uid;
+        FrameworkStatsLog.write(FrameworkStatsLog.LETTERBOX_POSITION_CHANGED, packageUid, position);
+
+        if (!mPackageUidToCompatStateInfo.contains(packageUid)) {
+            // There is no last logged activity for this packageUid so we should not log the
+            // position change as we can only log the position change for the current activity
+            return;
+        }
+        final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid);
+        final ActivityRecord lastLoggedActivity = compatStateInfo.mLastLoggedActivity;
+        if (activity != lastLoggedActivity) {
+            // Only log the position change for the current activity to be consistent with
+            // findAppCompatStateToLog and ensure that metrics for the state changes are computed
+            // correctly
+            return;
+        }
+        int state = activity.getAppCompatState();
+        logAppCompatStateInternal(activity, state, compatStateInfo);
+
+        if (DEBUG_METRICS) {
+            Slog.i(TAG, String.format("LETTERBOX_POSITION_CHANGED(%s, %s)",
+                    packageUid, position));
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b3b392c..62427e1 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2467,8 +2467,16 @@
         if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null
                 && mActivityComponent.equals(task.intent.getComponent())) {
             final ActivityRecord topAttached = task.getActivity(ActivityRecord::attachedToProcess);
-            if (topAttached != null && topAttached.isSnapshotCompatible(snapshot)) {
-                return STARTING_WINDOW_TYPE_SNAPSHOT;
+            if (topAttached != null) {
+                if (topAttached.isSnapshotCompatible(snapshot)
+                        // This trampoline must be the same rotation.
+                        && mDisplayContent.getDisplayRotation().rotationForOrientation(mOrientation,
+                                mDisplayContent.getRotation()) == snapshot.getRotation()) {
+                    return STARTING_WINDOW_TYPE_SNAPSHOT;
+                }
+                // No usable snapshot. And a splash screen may also be weird because an existing
+                // activity may be shown right after the trampoline is finished.
+                return STARTING_WINDOW_TYPE_NONE;
             }
         }
         final boolean isActivityHome = isActivityTypeHome();
@@ -3207,12 +3215,29 @@
             return false;
         }
 
-        if (mRootWindowContainer.getTopResumedActivity() == this
-                && getDisplayContent().mFocusedApp == this) {
-            ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top, "
-                    + "activity=%s", this);
-            return !isState(RESUMED);
+        // If this activity already positions on the top focused task, moving the task to front
+        // is not needed. But we still need to ensure this activity is focused because the
+        // current focused activity could be another activity in the same Task if activities are
+        // displayed on adjacent TaskFragments.
+        final ActivityRecord currentFocusedApp = mDisplayContent.mFocusedApp;
+        if (currentFocusedApp != null && currentFocusedApp.task == task) {
+            final Task topFocusableTask = mDisplayContent.getTask(
+                    (t) -> t.isLeafTask() && t.isFocusable(), true /*  traverseTopToBottom */);
+            if (task == topFocusableTask) {
+                if (currentFocusedApp == this) {
+                    ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: already on top "
+                            + "and focused, activity=%s", this);
+                } else {
+                    ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: set focused, "
+                            + "activity=%s", this);
+                    mDisplayContent.setFocusedApp(this);
+                    mAtmService.mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
+                            true /* updateInputWindows */);
+                }
+                return !isState(RESUMED);
+            }
         }
+
         ProtoLog.d(WM_DEBUG_FOCUS, "moveFocusableActivityToTop: activity=%s", this);
 
         rootTask.moveToFront(reason, task);
@@ -7788,11 +7813,15 @@
                 newParentConfiguration.windowConfiguration.getWindowingMode();
         final boolean isFixedOrientationLetterboxAllowed =
                 parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
-                        || parentWindowingMode == WINDOWING_MODE_FULLSCREEN;
+                        || parentWindowingMode == WINDOWING_MODE_FULLSCREEN
+                        // Switching from PiP to fullscreen.
+                        || (parentWindowingMode == WINDOWING_MODE_PINNED
+                                && resolvedConfig.windowConfiguration.getWindowingMode()
+                                        == WINDOWING_MODE_FULLSCREEN);
         // TODO(b/181207944): Consider removing the if condition and always run
         // resolveFixedOrientationConfiguration() since this should be applied for all cases.
         if (isFixedOrientationLetterboxAllowed) {
-            resolveFixedOrientationConfiguration(newParentConfiguration, parentWindowingMode);
+            resolveFixedOrientationConfiguration(newParentConfiguration);
         }
 
         if (mCompatDisplayInsets != null) {
@@ -8084,8 +8113,7 @@
      * <p>If letterboxed due to fixed orientation then aspect ratio restrictions are also applied
      * in this method.
      */
-    private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig,
-            int windowingMode) {
+    private void resolveFixedOrientationConfiguration(@NonNull Configuration newParentConfig) {
         mLetterboxBoundsForFixedOrientationAndAspectRatio = null;
         mIsEligibleForFixedOrientationLetterbox = false;
         final Rect parentBounds = newParentConfig.windowConfiguration.getBounds();
@@ -8105,11 +8133,6 @@
         if (organizedTf != null && !organizedTf.fillsParent()) {
             return;
         }
-        if (windowingMode == WINDOWING_MODE_PINNED) {
-            // PiP bounds have higher priority than the requested orientation. Otherwise the
-            // activity may be squeezed into a small piece.
-            return;
-        }
 
         final Rect resolvedBounds =
                 getResolvedOverrideConfiguration().windowConfiguration.getBounds();
@@ -8174,7 +8197,7 @@
         resolvedBounds.set(containingBounds);
 
         final float letterboxAspectRatioOverride =
-                mWmService.mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+                mLetterboxUiController.getFixedOrientationLetterboxAspectRatio();
         final float desiredAspectRatio =
                 letterboxAspectRatioOverride > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
                         ? letterboxAspectRatioOverride : computeAspectRatio(parentBounds);
@@ -8727,18 +8750,7 @@
      * Returns the min aspect ratio of this activity.
      */
     private float getMinAspectRatio() {
-        float infoAspectRatio = info.getMinAspectRatio(getRequestedOrientation());
-        // Complying with the CDD 7.1.1.2 requirement for unresizble apps:
-        // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio
-        return infoAspectRatio < 1f && info.resizeMode == RESIZE_MODE_UNRESIZEABLE
-                    // TODO(233582832): Consider removing fixed-orientation condition.
-                    // Some apps switching from tablet to phone layout at the certain size
-                    // threshold. This may lead to flickering on tablets in landscape orientation
-                    // if an app sets orientation to portrait dynamically because of aspect ratio
-                    // restriction applied here.
-                    && getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
-                ? mLetterboxUiController.getDefaultMinAspectRatioForUnresizableApps()
-                : infoAspectRatio;
+        return info.getMinAspectRatio(getRequestedOrientation());
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a273529..a7c09a4 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2565,6 +2565,7 @@
             mInTask = null;
         }
         mInTaskFragment = inTaskFragment;
+        sendNewTaskFragmentResultRequestIfNeeded();
 
         mStartFlags = startFlags;
         // If the onlyIfNeeded flag is set, then we can do this if the activity being launched
@@ -2607,6 +2608,18 @@
         }
     }
 
+    private void sendNewTaskFragmentResultRequestIfNeeded() {
+        if (mStartActivity.resultTo != null && mInTaskFragment != null
+                && mInTaskFragment != mStartActivity.resultTo.getTaskFragment()) {
+            Slog.w(TAG,
+                    "Activity is launching as a new TaskFragment, so cancelling activity result.");
+            mStartActivity.resultTo.sendResult(INVALID_UID, mStartActivity.resultWho,
+                    mStartActivity.requestCode, RESULT_CANCELED,
+                    null /* data */, null /* dataGrants */);
+            mStartActivity.resultTo = null;
+        }
+    }
+
     private void computeLaunchingTaskFlags() {
         // If the caller is not coming from another activity, but has given us an explicit task into
         // which they would like us to launch the new activity, then let's see about doing that.
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 3d66122..fb9d7e6 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -116,7 +116,6 @@
     private final DisplayContent mDisplayContent;
     private final WallpaperController mWallpaperControllerLocked;
     private RemoteAnimationDefinition mRemoteAnimationDefinition = null;
-    private static final int KEYGUARD_GOING_AWAY_ANIMATION_DURATION = 400;
 
     private static final int TYPE_NONE = 0;
     private static final int TYPE_ACTIVITY = 1;
@@ -727,14 +726,17 @@
      */
     private void overrideWithRemoteAnimationIfSet(@Nullable ActivityRecord animLpActivity,
             @TransitionOldType int transit, ArraySet<Integer> activityTypes) {
+        RemoteAnimationAdapter adapter = null;
         if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) {
             // The crash transition has higher priority than any involved remote animations.
-            return;
+        } else if (AppTransition.isKeyguardGoingAwayTransitOld(transit)) {
+            adapter = mRemoteAnimationDefinition != null
+                    ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes)
+                    : null;
+        } else if (mDisplayContent.mAppTransition.getRemoteAnimationController() == null) {
+            adapter = getRemoteAnimationOverride(animLpActivity, transit, activityTypes);
         }
-        final RemoteAnimationAdapter adapter =
-                getRemoteAnimationOverride(animLpActivity, transit, activityTypes);
-        if (adapter != null
-                && mDisplayContent.mAppTransition.getRemoteAnimationController() == null) {
+        if (adapter != null) {
             mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
         }
     }
diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java
index fca4942..fff7637 100644
--- a/services/core/java/com/android/server/wm/ContentRecordingController.java
+++ b/services/core/java/com/android/server/wm/ContentRecordingController.java
@@ -63,6 +63,7 @@
      */
     void setContentRecordingSessionLocked(@Nullable ContentRecordingSession incomingSession,
             @NonNull WindowManagerService wmService) {
+        // TODO(b/219761722) handle a null session arriving due to task setup failing
         if (incomingSession != null && (!ContentRecordingSession.isValid(incomingSession)
                 || ContentRecordingSession.isSameDisplay(mSession, incomingSession))) {
             // Ignore an invalid session, or a session for the same display as currently recording.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ad3b8ee..288777b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1610,7 +1610,8 @@
         if (mTransitionController.useShellTransitionsRotation()) {
             return ROTATION_UNDEFINED;
         }
-        if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM) {
+        if (!WindowManagerService.ENABLE_FIXED_ROTATION_TRANSFORM
+                || getIgnoreOrientationRequest()) {
             return ROTATION_UNDEFINED;
         }
         if (r.mOrientation == ActivityInfo.SCREEN_ORIENTATION_BEHIND) {
@@ -2941,9 +2942,10 @@
             // Set some sort of reasonable bounds on the size of the display that we will try
             // to emulate.
             final int minSize = 200;
-            final int maxScale = 2;
-            width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale);
-            height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale);
+            final int maxScale = 3;
+            final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale;
+            width = Math.min(Math.max(width, minSize), maxSize);
+            height = Math.min(Math.max(height, minSize), maxSize);
         }
 
         Slog.i(TAG_WM, "Using new display size: " + width + "x" + height);
@@ -6193,6 +6195,14 @@
                 .getKeyguardController().isAodShowing(mDisplayId);
     }
 
+    /**
+     * @return whether the keyguard is occluded on this display
+     */
+    boolean isKeyguardOccluded() {
+        return mRootWindowContainer.mTaskSupervisor
+                .getKeyguardController().isDisplayOccluded(mDisplayId);
+    }
+
     @VisibleForTesting
     void removeAllTasks() {
         forAllTasks((t) -> { t.getRootTask().removeChild(t, "removeAllTasks"); });
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5c1fc65..cff8b93 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1212,7 +1212,8 @@
                 break;
             default:
                 if (attrs.providedInsets != null) {
-                    for (InsetsFrameProvider provider : attrs.providedInsets) {
+                    for (int i = attrs.providedInsets.length - 1; i >= 0; i--) {
+                        final InsetsFrameProvider provider = attrs.providedInsets[i];
                         switch (provider.type) {
                             case ITYPE_STATUS_BAR:
                                 mStatusBarAlt = win;
@@ -1231,21 +1232,29 @@
                                 mExtraNavBarAltPosition = getAltBarPosition(attrs);
                                 break;
                         }
+                        // The index of the provider and corresponding insets types cannot change at
+                        // runtime as ensured in WMS. Make use of the index in the provider directly
+                        // to access the latest provided size at runtime.
+                        final int index = i;
                         final TriConsumer<DisplayFrames, WindowContainer, Rect> frameProvider =
                                 provider.insetsSize != null
                                         ? (displayFrames, windowContainer, inOutFrame) -> {
                                             inOutFrame.inset(win.mGivenContentInsets);
+                                            final InsetsFrameProvider ifp =
+                                                    win.mAttrs.forRotation(displayFrames.mRotation)
+                                                            .providedInsets[index];
                                             calculateInsetsFrame(displayFrames, windowContainer,
-                                                    inOutFrame, provider.source,
-                                                    provider.insetsSize);
+                                                    inOutFrame, ifp.source, ifp.insetsSize);
                                         } : null;
                         final TriConsumer<DisplayFrames, WindowContainer, Rect> imeFrameProvider =
                                 provider.imeInsetsSize != null
                                         ? (displayFrames, windowContainer, inOutFrame) -> {
                                             inOutFrame.inset(win.mGivenContentInsets);
+                                            final InsetsFrameProvider ifp =
+                                                    win.mAttrs.forRotation(displayFrames.mRotation)
+                                                            .providedInsets[index];
                                             calculateInsetsFrame(displayFrames, windowContainer,
-                                                    inOutFrame, provider.source,
-                                                    provider.imeInsetsSize);
+                                                    inOutFrame, ifp.source, ifp.imeInsetsSize);
                                         } : null;
                         mDisplayContent.setInsetProvider(provider.type, win, frameProvider,
                                 imeFrameProvider);
@@ -1256,14 +1265,14 @@
         }
     }
 
-    private void calculateInsetsFrame(DisplayFrames df, WindowContainer coutainer, Rect inOutFrame,
+    private void calculateInsetsFrame(DisplayFrames df, WindowContainer container, Rect inOutFrame,
             int source, Insets insetsSize) {
         if (source == InsetsFrameProvider.SOURCE_DISPLAY) {
             inOutFrame.set(df.mUnrestricted);
         } else if (source == InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS) {
-            inOutFrame.set(coutainer.getBounds());
+            inOutFrame.set(container.getBounds());
         }
-        if (insetsSize == null || insetsSize.equals(Insets.NONE)) {
+        if (insetsSize == null) {
             return;
         }
         // Only one side of the provider shall be applied. Check in the order of left - top -
@@ -1276,6 +1285,8 @@
             inOutFrame.left = inOutFrame.right - insetsSize.right;
         } else if (insetsSize.bottom != 0) {
             inOutFrame.top = inOutFrame.bottom - insetsSize.bottom;
+        } else {
+            inOutFrame.setEmpty();
         }
     }
 
@@ -1523,13 +1534,14 @@
      */
     void simulateLayoutDisplay(DisplayFrames displayFrames) {
         final InsetsStateController controller = mDisplayContent.getInsetsStateController();
+        sTmpClientFrames.attachedFrame = null;
         for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) {
             final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i);
             mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation),
                     displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe,
                     displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH,
-                    UNSPECIFIED_LENGTH, win.getRequestedVisibilities(),
-                    null /* attachedWindowFrame */, win.mGlobalScale, sTmpClientFrames);
+                    UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale,
+                    sTmpClientFrames);
             final SparseArray<InsetsSource> sources = win.getProvidedInsetsSources();
             final InsetsState state = displayFrames.mInsetsState;
             for (int index = sources.size() - 1; index >= 0; index--) {
@@ -1541,13 +1553,14 @@
     }
 
     void updateInsetsSourceFramesExceptIme(DisplayFrames displayFrames) {
+        sTmpClientFrames.attachedFrame = null;
         for (int i = mInsetsSourceWindowsExceptIme.size() - 1; i >= 0; i--) {
             final WindowState win = mInsetsSourceWindowsExceptIme.valueAt(i);
             mWindowLayout.computeFrames(win.mAttrs.forRotation(displayFrames.mRotation),
                     displayFrames.mInsetsState, displayFrames.mDisplayCutoutSafe,
                     displayFrames.mUnrestricted, win.getWindowingMode(), UNSPECIFIED_LENGTH,
-                    UNSPECIFIED_LENGTH, win.getRequestedVisibilities(),
-                    null /* attachedWindowFrame */, win.mGlobalScale, sTmpClientFrames);
+                    UNSPECIFIED_LENGTH, win.getRequestedVisibilities(), win.mGlobalScale,
+                    sTmpClientFrames);
             win.updateSourceFrame(sTmpClientFrames.frame);
         }
     }
@@ -1577,7 +1590,7 @@
         displayFrames = win.getDisplayFrames(displayFrames);
 
         final WindowManager.LayoutParams attrs = win.mAttrs.forRotation(displayFrames.mRotation);
-        final Rect attachedWindowFrame = attached != null ? attached.getFrame() : null;
+        sTmpClientFrames.attachedFrame = attached != null ? attached.getFrame() : null;
 
         // If this window has different LayoutParams for rotations, we cannot trust its requested
         // size. Because it might have not sent its requested size for the new rotation.
@@ -1587,8 +1600,7 @@
 
         mWindowLayout.computeFrames(attrs, win.getInsetsState(), displayFrames.mDisplayCutoutSafe,
                 win.getBounds(), win.getWindowingMode(), requestedWidth, requestedHeight,
-                win.getRequestedVisibilities(), attachedWindowFrame, win.mGlobalScale,
-                sTmpClientFrames);
+                win.getRequestedVisibilities(), win.mGlobalScale, sTmpClientFrames);
 
         win.setFrames(sTmpClientFrames, win.mRequestedWidth, win.mRequestedHeight);
     }
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index d2c71f5..b9d8319 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -620,7 +620,8 @@
         // We only enable seamless rotation if the top window has requested it and is in the
         // fullscreen opaque state. Seamless rotation requires freezing various Surface states and
         // won't work well with animations, so we disable it in the animation case for now.
-        if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.isAnimatingLw()) {
+        if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.inMultiWindowMode()
+                || w.isAnimatingLw()) {
             return false;
         }
 
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 2d227b6..91b2fb6 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -37,11 +37,6 @@
      */
     static final float MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO = 1.0f;
 
-    // Min allowed aspect ratio for unresizable apps which is used when an app doesn't specify
-    // android:minAspectRatio in accordance with the CDD 7.1.1.2 requirement:
-    // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio
-    static final float MIN_UNRESIZABLE_ASPECT_RATIO = 4 / 3f;
-
     /** Enum for Letterbox background type. */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({LETTERBOX_BACKGROUND_SOLID_COLOR, LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND,
@@ -109,9 +104,7 @@
     // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored.
     private float mFixedOrientationLetterboxAspectRatio;
 
-    // Default min aspect ratio for unresizable apps which is used when an app doesn't specify
-    // android:minAspectRatio in accordance with the CDD 7.1.1.2 requirement:
-    // https://source.android.com/compatibility/12/android-12-cdd#7112_screen_aspect_ratio
+    // Default min aspect ratio for unresizable apps that are eligible for the size compat mode.
     private float mDefaultMinAspectRatioForUnresizableApps;
 
     // Corners radius for activities presented in the letterbox mode, values < 0 will be ignored.
@@ -250,13 +243,7 @@
     }
 
     /**
-     * Resets the min aspect ratio for unresizable apps which is used when an app doesn't specify
-     * {@code android:minAspectRatio} to {@link
-     * R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps}.
-     *
-     * @throws AssertionError if {@link
-     * R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps} is < {@link
-     * #MIN_UNRESIZABLE_ASPECT_RATIO}.
+     * Resets the min aspect ratio for unresizable apps that are eligible for size compat mode.
      */
     void resetDefaultMinAspectRatioForUnresizableApps() {
         setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
@@ -264,25 +251,16 @@
     }
 
     /**
-     * Gets the min aspect ratio for unresizable apps which is used when an app doesn't specify
-     * {@code android:minAspectRatio}.
+     * Gets the min aspect ratio for unresizable apps that are eligible for size compat mode.
      */
     float getDefaultMinAspectRatioForUnresizableApps() {
         return mDefaultMinAspectRatioForUnresizableApps;
     }
 
     /**
-     * Overrides the min aspect ratio for unresizable apps which is used when an app doesn't
-     * specify {@code android:minAspectRatio}.
-     *
-     * @throws AssertionError if given value is < {@link #MIN_UNRESIZABLE_ASPECT_RATIO}.
+     * Overrides the min aspect ratio for unresizable apps that are eligible for size compat mode.
      */
     void setDefaultMinAspectRatioForUnresizableApps(float aspectRatio) {
-        if (aspectRatio < MIN_UNRESIZABLE_ASPECT_RATIO) {
-            throw new AssertionError(
-                    "Unexpected min aspect ratio for unresizable apps, it should be <= "
-                            + MIN_UNRESIZABLE_ASPECT_RATIO + " but was " + aspectRatio);
-        }
         mDefaultMinAspectRatioForUnresizableApps = aspectRatio;
     }
 
@@ -709,6 +687,24 @@
         }
     }
 
+    /*
+     * Gets the horizontal position of the letterboxed app window when horizontal reachability is
+     * enabled.
+     */
+    @LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        return mLetterboxPositionForHorizontalReachability;
+    }
+
+    /*
+     * Gets the vertical position of the letterboxed app window when vertical reachability is
+     * enabled.
+     */
+    @LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        return mLetterboxPositionForVerticalReachability;
+    }
+
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
     static String letterboxHorizontalReachabilityPositionToString(
             @LetterboxHorizontalReachabilityPosition int position) {
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index df9a87e..d652767 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -21,6 +21,20 @@
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
 import static com.android.server.wm.ActivityRecord.computeAspectRatio;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -28,6 +42,13 @@
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
 import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
 import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTypeToString;
 
 import android.annotation.Nullable;
@@ -211,10 +232,19 @@
                 : mLetterboxConfiguration.getLetterboxVerticalPositionMultiplier();
     }
 
-    float getDefaultMinAspectRatioForUnresizableApps() {
+    float getFixedOrientationLetterboxAspectRatio() {
+        return mActivityRecord.shouldCreateCompatDisplayInsets()
+                ? getDefaultMinAspectRatioForUnresizableApps()
+                : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
+    }
+
+    private float getDefaultMinAspectRatioForUnresizableApps() {
         if (!mLetterboxConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled()
                 || mActivityRecord.getDisplayContent() == null) {
-            return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps();
+            return mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
+                    > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO
+                            ? mLetterboxConfiguration.getDefaultMinAspectRatioForUnresizableApps()
+                            : mLetterboxConfiguration.getFixedOrientationLetterboxAspectRatio();
         }
 
         int dividerWindowWidth =
@@ -226,10 +256,10 @@
         // Getting the same aspect ratio that apps get in split screen.
         Rect bounds = new Rect(mActivityRecord.getDisplayContent().getBounds());
         if (bounds.width() >= bounds.height()) {
-            bounds.inset(/* dx */ dividerSize, /* dy */ 0);
+            bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
             bounds.right = bounds.centerX();
         } else {
-            bounds.inset(/* dx */ 0, /* dy */ dividerSize);
+            bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
             bounds.bottom = bounds.centerY();
         }
         return computeAspectRatio(bounds);
@@ -249,12 +279,26 @@
             return;
         }
 
+        int letterboxPositionForHorizontalReachability = mLetterboxConfiguration
+                .getLetterboxPositionForHorizontalReachability();
         if (mLetterbox.getInnerFrame().left > x) {
             // Moving to the next stop on the left side of the app window: right > center > left.
             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextLeftStop();
+            int changeToLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__RIGHT_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().right < x) {
             // Moving to the next stop on the right side of the app window: left > center > right.
             mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+            int changeToLog =
+                    letterboxPositionForHorizontalReachability
+                            == LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_RIGHT
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__LEFT_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         }
 
         // TODO(197549949): Add animation for transition.
@@ -270,13 +314,26 @@
             // Only react to clicks at the top and bottom of the letterboxed app window.
             return;
         }
-
+        int letterboxPositionForVerticalReachability = mLetterboxConfiguration
+                .getLetterboxPositionForVerticalReachability();
         if (mLetterbox.getInnerFrame().top > y) {
             // Moving to the next stop on the top side of the app window: bottom > center > top.
             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextTopStop();
+            int changeToLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_TOP
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         } else if (mLetterbox.getInnerFrame().bottom < y) {
             // Moving to the next stop on the bottom side of the app window: top > center > bottom.
             mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+            int changeToLog =
+                    letterboxPositionForVerticalReachability
+                            == LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER
+                                ? LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM
+                                : LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__TOP_TO_CENTER;
+            logLetterboxPositionChange(changeToLog);
         }
 
         // TODO(197549949): Add animation for transition.
@@ -567,4 +624,63 @@
         return "UNKNOWN_REASON";
     }
 
+    private int letterboxHorizontalReachabilityPositionToLetterboxPosition(
+            @LetterboxConfiguration.LetterboxHorizontalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__LEFT;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox horizontal reachability position type: "
+                                + position);
+        }
+    }
+
+    private int letterboxVerticalReachabilityPositionToLetterboxPosition(
+            @LetterboxConfiguration.LetterboxVerticalReachabilityPosition int position) {
+        switch (position) {
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER;
+            case LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM:
+                return APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM;
+            default:
+                throw new AssertionError(
+                        "Unexpected letterbox vertical reachability position type: "
+                                + position);
+        }
+    }
+
+    int getLetterboxPositionForLogging() {
+        int positionToLog = APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION;
+        if (isHorizontalReachabilityEnabled()) {
+            int letterboxPositionForHorizontalReachability = getLetterboxConfiguration()
+                    .getLetterboxPositionForHorizontalReachability();
+            positionToLog = letterboxHorizontalReachabilityPositionToLetterboxPosition(
+                            letterboxPositionForHorizontalReachability);
+        } else if (isVerticalReachabilityEnabled()) {
+            int letterboxPositionForVerticalReachability = getLetterboxConfiguration()
+                    .getLetterboxPositionForVerticalReachability();
+            positionToLog = letterboxVerticalReachabilityPositionToLetterboxPosition(
+                            letterboxPositionForVerticalReachability);
+        }
+        return positionToLog;
+    }
+
+    private LetterboxConfiguration getLetterboxConfiguration() {
+        return mLetterboxConfiguration;
+    }
+
+    /**
+     * Logs letterbox position changes via {@link ActivityMetricsLogger#logLetterboxPositionChange}.
+     */
+    private void logLetterboxPositionChange(int letterboxPositionChange) {
+        mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
+                .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index ad158c7..ac1a2b1 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -331,8 +331,10 @@
 
     private void invokeAnimationCancelled(String reason) {
         ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason);
+        final boolean isKeyguardOccluded = mDisplayContent.isKeyguardOccluded();
+
         try {
-            mRemoteAnimationAdapter.getRunner().onAnimationCancelled();
+            mRemoteAnimationAdapter.getRunner().onAnimationCancelled(isKeyguardOccluded);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to notify cancel", e);
         }
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 30b5083..9b013da 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -115,8 +115,6 @@
     private float mLastReportedAnimatorScale;
     private String mPackageName;
     private String mRelayoutTag;
-    private String mUpdateViewVisibilityTag;
-    private String mUpdateWindowLayoutTag;
     private final InsetsVisibilities mDummyRequestedVisibilities = new InsetsVisibilities();
     private final InsetsSourceControl[] mDummyControls =  new InsetsSourceControl[0];
     final boolean mSetsUnrestrictedKeepClearAreas;
@@ -195,27 +193,28 @@
     public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
             int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
+            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
                 UserHandle.getUserId(mUid), requestedVisibilities, outInputChannel, outInsetsState,
-                outActiveControls);
+                outActiveControls, outAttachedFrame);
     }
 
     @Override
     public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
             int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
+            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,
-                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls);
+                requestedVisibilities, outInputChannel, outInsetsState, outActiveControls,
+                outAttachedFrame);
     }
 
     @Override
     public int addToDisplayWithoutInputChannel(IWindow window, WindowManager.LayoutParams attrs,
-            int viewVisibility, int displayId, InsetsState outInsetsState) {
+            int viewVisibility, int displayId, InsetsState outInsetsState, Rect outAttachedFrame) {
         return mService.addWindow(this, window, attrs, viewVisibility, displayId,
                 UserHandle.getUserId(mUid), mDummyRequestedVisibilities, null /* outInputChannel */,
-                outInsetsState, mDummyControls);
+                outInsetsState, mDummyControls, outAttachedFrame);
     }
 
     @Override
@@ -224,32 +223,16 @@
     }
 
     @Override
-    public int updateVisibility(IWindow client, WindowManager.LayoutParams attrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateViewVisibilityTag);
-        int res = mService.updateViewVisibility(this, client, attrs, viewVisibility,
-                outMergedConfiguration, outSurfaceControl, outInsetsState, outActiveControls);
-        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-        return res;
-    }
-
-    @Override
-    public void updateLayout(IWindow window, WindowManager.LayoutParams attrs, int flags,
-            ClientWindowFrames clientFrames, int requestedWidth, int requestedHeight) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mUpdateWindowLayoutTag);
-        mService.updateWindowLayout(this, window, attrs, flags, clientFrames, requestedWidth,
-                requestedHeight);
-        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-    }
-
-    @Override
     public void prepareToReplaceWindows(IBinder appToken, boolean childrenOnly) {
         mService.setWillReplaceWindows(appToken, childrenOnly);
     }
 
     @Override
+    public boolean cancelDraw(IWindow window) {
+        return mService.cancelDraw(this, window);
+    }
+
+    @Override
     public int relayout(IWindow window, WindowManager.LayoutParams attrs,
             int requestedWidth, int requestedHeight, int viewFlags, int flags,
             ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
@@ -711,8 +694,6 @@
             if (wpc != null) {
                 mPackageName = wpc.mInfo.packageName;
                 mRelayoutTag = "relayoutWindow: " + mPackageName;
-                mUpdateViewVisibilityTag = "updateVisibility: " + mPackageName;
-                mUpdateWindowLayoutTag = "updateLayout: " + mPackageName;
             } else {
                 Slog.e(TAG_WM, "Unknown process pid=" + mPid);
             }
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index 68dbb06..0bb773a 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -158,14 +158,13 @@
                         + topFullscreenActivity);
                 return null;
             }
-            if (topFullscreenActivity.getWindowConfiguration().getRotation()
-                    != taskSnapshot.getRotation()) {
+            if (activity.mDisplayContent.getRotation() != taskSnapshot.getRotation()) {
                 // The snapshot should have been checked by ActivityRecord#isSnapshotCompatible
                 // that the activity will be updated to the same rotation as the snapshot. Since
                 // the transition is not started yet, fixed rotation transform needs to be applied
                 // earlier to make the snapshot show in a rotated container.
                 activity.mDisplayContent.handleTopActivityLaunchingInDifferentOrientation(
-                        topFullscreenActivity, false /* checkOpening */);
+                        activity, false /* checkOpening */);
             }
                 mService.mAtmService.mTaskOrganizerController.addStartingWindow(task,
                         activity, 0 /* launchTheme */, taskSnapshot);
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 36464b8..8220cae 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1114,7 +1114,10 @@
         // If a task is launching from a created-by-organizer task, it should be launched into the
         // same created-by-organizer task as well. Unless, the candidate task is already positioned
         // in the another adjacent task.
-        if (sourceTask != null) {
+        if (sourceTask != null && (candidateTask == null
+                // A pinned task relaunching should be handled by its task organizer. Skip fallback
+                // launch target of a pinned task from source task.
+                || candidateTask.getWindowingMode() != WINDOWING_MODE_PINNED)) {
             Task launchTarget = sourceTask.getCreatedByOrganizerTask();
             if (launchTarget != null && launchTarget.getAdjacentTaskFragment() != null) {
                 if (candidateTask != null) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3c0cac0..ae61f24 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -149,9 +149,6 @@
 
     final @TransitionType int mType;
     private int mSyncId = -1;
-    // Used for tracking a Transition throughout a lifecycle (i.e. from STATE_COLLECTING to
-    // STATE_FINISHED or STATE_ABORT), and should only be used for testing and debugging.
-    private int mDebugId = -1;
     private @TransitionFlags int mFlags;
     private final TransitionController mController;
     private final BLASTSyncEngine mSyncEngine;
@@ -295,11 +292,6 @@
         return mSyncId;
     }
 
-    @VisibleForTesting
-    int getDebugId() {
-        return mDebugId;
-    }
-
     @TransitionFlags
     int getFlags() {
         return mFlags;
@@ -315,6 +307,10 @@
         return mFinishTransaction;
     }
 
+    private boolean isCollecting() {
+        return mState == STATE_COLLECTING || mState == STATE_STARTED;
+    }
+
     /** Starts collecting phase. Once this starts, all relevant surface operations are sync. */
     void startCollecting(long timeoutMs) {
         if (mState != STATE_PENDING) {
@@ -322,7 +318,6 @@
         }
         mState = STATE_COLLECTING;
         mSyncId = mSyncEngine.startSyncSet(this, timeoutMs, TAG);
-        mDebugId = mSyncId;
 
         mController.mTransitionTracer.logState(this);
     }
@@ -353,7 +348,10 @@
         if (mState < STATE_COLLECTING) {
             throw new IllegalStateException("Transition hasn't started collecting.");
         }
-        if (mSyncId < 0) return;
+        if (!isCollecting()) {
+            // Too late, transition already started playing, so don't collect.
+            return;
+        }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s",
                 mSyncId, wc);
         // "snapshot" all parents (as potential promotion targets). Do this before checking
@@ -403,7 +401,10 @@
      * or waiting until after the animation to close).
      */
     void collectExistenceChange(@NonNull WindowContainer wc) {
-        if (mSyncId < 0) return;
+        if (mState >= STATE_PLAYING) {
+            // Too late to collect. Don't check too-early here since `collect` will check that.
+            return;
+        }
         ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Existence Changed in transition %d:"
                 + " %s", mSyncId, wc);
         collect(wc);
@@ -437,7 +438,7 @@
      */
     void setOverrideAnimation(TransitionInfo.AnimationOptions options,
             @Nullable IRemoteCallback startCallback, @Nullable IRemoteCallback finishCallback) {
-        if (mSyncId < 0) return;
+        if (!isCollecting()) return;
         mOverrideOptions = options;
         sendRemoteCallback(mClientAnimationStartCallback);
         mClientAnimationStartCallback = startCallback;
@@ -455,7 +456,7 @@
      *           The transition will wait for all groups to be ready.
      */
     void setReady(WindowContainer wc, boolean ready) {
-        if (mSyncId < 0) return;
+        if (!isCollecting() || mSyncId < 0) return;
         mReadyTracker.setReadyFrom(wc, ready);
         applyReady();
     }
@@ -473,7 +474,7 @@
      * @see ReadyTracker#setAllReady.
      */
     void setAllReady() {
-        if (mSyncId < 0) return;
+        if (!isCollecting() || mSyncId < 0) return;
         mReadyTracker.setAllReady();
         applyReady();
     }
@@ -672,7 +673,7 @@
         SurfaceControl.Transaction inputSinkTransaction = null;
         for (int i = 0; i < mParticipants.size(); ++i) {
             final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
-            if (ar == null || !ar.isVisible()) continue;
+            if (ar == null || !ar.isVisible() || ar.getParent() == null) continue;
             if (inputSinkTransaction == null) {
                 inputSinkTransaction = new SurfaceControl.Transaction();
             }
@@ -889,7 +890,6 @@
             // No player registered, so just finish/apply immediately
             cleanUpOnFailure();
         }
-        mSyncId = -1;
         mOverrideOptions = null;
 
         reportStartReasonsToLogger();
@@ -1614,7 +1614,7 @@
     }
 
     boolean getLegacyIsReady() {
-        return (mState == STATE_STARTED || mState == STATE_COLLECTING) && mSyncId >= 0;
+        return isCollecting() && mSyncId >= 0;
     }
 
     static Transition fromBinder(IBinder binder) {
diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java
index b1951e0..c1927d8 100644
--- a/services/core/java/com/android/server/wm/TransitionTracer.java
+++ b/services/core/java/com/android/server/wm/TransitionTracer.java
@@ -79,7 +79,7 @@
             final ProtoOutputStream outputStream = new ProtoOutputStream();
             final long transitionEntryToken = outputStream.start(TRANSITION);
 
-            outputStream.write(ID, transition.getDebugId());
+            outputStream.write(ID, transition.getSyncId());
             outputStream.write(TIMESTAMP, SystemClock.elapsedRealtimeNanos());
             outputStream.write(TRANSITION_TYPE, transition.mType);
             outputStream.write(STATE, transition.getState());
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 5d2e34b..b5abd32 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -48,6 +48,7 @@
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES;
 import static android.provider.Settings.Global.DEVELOPMENT_RENDER_SHADOWS_IN_COMPOSITOR;
 import static android.provider.Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_PATH;
+import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
@@ -89,6 +90,7 @@
 import static android.view.WindowManager.TRANSIT_NONE;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
 import static android.view.WindowManagerGlobal.ADD_OKAY;
+import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW;
 import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED;
 import static android.view.WindowManagerPolicyConstants.NAV_BAR_INVALID;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_MULTIPLIER;
@@ -287,6 +289,7 @@
 import android.window.ClientWindowFrames;
 import android.window.ITaskFpsCallback;
 import android.window.TaskSnapshot;
+import android.window.WindowContainerToken;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -1445,7 +1448,7 @@
     public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
             int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
             InputChannel outInputChannel, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
+            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame) {
         Arrays.fill(outActiveControls, null);
         int[] appOp = new int[1];
         final boolean isRoundedCornerOverlay = (attrs.privateFlags
@@ -1860,6 +1863,13 @@
 
             outInsetsState.set(win.getCompatInsetsState(), win.isClientLocal());
             getInsetsSourceControls(win, outActiveControls);
+
+            if (win.mLayoutAttached) {
+                outAttachedFrame.set(win.getParentWindow().getCompatFrame());
+            } else {
+                // Make this invalid which indicates a null attached frame.
+                outAttachedFrame.set(0, 0, -1, -1);
+            }
         }
 
         Binder.restoreCallingIdentity(origId);
@@ -2212,6 +2222,20 @@
                         == PackageManager.PERMISSION_GRANTED;
     }
 
+    /**
+     * Returns whether this window can proceed with drawing or needs to retry later.
+     */
+    public boolean cancelDraw(Session session, IWindow client) {
+        synchronized (mGlobalLock) {
+            final WindowState win = windowForClientLocked(session, client, false);
+            if (win == null) {
+                return false;
+            }
+
+            return win.cancelAndRedraw();
+        }
+    }
+
     public int relayoutWindow(Session session, IWindow client, LayoutParams attrs,
             int requestedWidth, int requestedHeight, int viewVisibility, int flags,
             ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
@@ -2228,6 +2252,11 @@
             if (win == null) {
                 return 0;
             }
+
+            if (win.cancelAndRedraw() && win.mPrepareSyncSeqId <= win.mLastSeqIdSentToRelayout) {
+                result |= RELAYOUT_RES_CANCEL_AND_REDRAW;
+            }
+
             final DisplayContent displayContent = win.getDisplayContent();
             final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
 
@@ -2642,29 +2671,6 @@
         return result;
     }
 
-    int updateViewVisibility(Session session, IWindow client, LayoutParams attrs,
-            int viewVisibility, MergedConfiguration outMergedConfiguration,
-            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
-            InsetsSourceControl[] outActiveControls) {
-        // TODO(b/161810301): Finish the implementation.
-        return 0;
-    }
-
-    void updateWindowLayout(Session session, IWindow client, LayoutParams attrs, int flags,
-            ClientWindowFrames clientWindowFrames, int requestedWidth, int requestedHeight) {
-        final long origId = Binder.clearCallingIdentity();
-        synchronized (mGlobalLock) {
-            final WindowState win = windowForClientLocked(session, client, false);
-            if (win == null) {
-                return;
-            }
-            win.setFrames(clientWindowFrames, requestedWidth, requestedHeight);
-
-            // TODO(b/161810301): Finish the implementation.
-        }
-        Binder.restoreCallingIdentity(origId);
-    }
-
     public boolean outOfMemoryWindow(Session session, IWindow client) {
         final long origId = Binder.clearCallingIdentity();
 
@@ -8277,6 +8283,26 @@
         @Override
         public void setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
             synchronized (mGlobalLock) {
+                // Allow the controller to handle teardown or a non-task session.
+                if (incomingSession == null
+                        || incomingSession.getContentToRecord() != RECORD_CONTENT_TASK) {
+                    mContentRecordingController.setContentRecordingSessionLocked(incomingSession,
+                            WindowManagerService.this);
+                    return;
+                }
+                // For a task session, find the activity identified by the launch cookie.
+                final WindowContainerToken wct = getTaskWindowContainerTokenForLaunchCookie(
+                        incomingSession.getTokenToRecord());
+                if (wct == null) {
+                    Slog.w(TAG, "Handling a new recording session; unable to find the "
+                            + "WindowContainerToken");
+                    mContentRecordingController.setContentRecordingSessionLocked(null,
+                            WindowManagerService.this);
+                    return;
+                }
+                // Replace the launch cookie in the session details with the task's
+                // WindowContainerToken.
+                incomingSession.setTokenToRecord(wct.asBinder());
                 mContentRecordingController.setContentRecordingSessionLocked(incomingSession,
                         WindowManagerService.this);
             }
@@ -8535,6 +8561,38 @@
     }
 
     /**
+     * Retrieve the {@link WindowContainerToken} of the task that contains the activity started
+     * with the given launch cookie.
+     *
+     * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an
+     *                     activity
+     * @return a token representing the task containing the activity started with the given launch
+     * cookie, or {@code null} if the token couldn't be found.
+     */
+    @VisibleForTesting
+    @Nullable
+    WindowContainerToken getTaskWindowContainerTokenForLaunchCookie(@NonNull IBinder launchCookie) {
+        // Find the activity identified by the launch cookie.
+        final ActivityRecord targetActivity = mRoot.getActivity(
+                activity -> activity.mLaunchCookie == launchCookie);
+        if (targetActivity == null) {
+            Slog.w(TAG, "Unable to find the activity for this launch cookie");
+            return null;
+        }
+        if (targetActivity.getTask() == null) {
+            Slog.w(TAG, "Unable to find the task for this launch cookie");
+            return null;
+        }
+        WindowContainerToken taskWindowContainerToken =
+                targetActivity.getTask().mRemoteToken.toWindowContainerToken();
+        if (taskWindowContainerToken == null) {
+            Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName());
+            return null;
+        }
+        return taskWindowContainerToken;
+    }
+
+    /**
      * You need ALLOW_SLIPPERY_TOUCHES permission to be able to set FLAG_SLIPPERY.
      */
     private int sanitizeFlagSlippery(int flags, String windowName, int callingUid, int callingPid) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 02f056c..ff43a96 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -1370,9 +1370,10 @@
         pw.println("        be ignored and framework implementation will determine aspect ratio.");
         pw.println("      --minAspectRatioForUnresizable aspectRatio");
         pw.println("        Default min aspect ratio for unresizable apps which is used when an");
-        pw.println("        app doesn't specify android:minAspectRatio. An exception will be");
-        pw.println("        thrown if aspectRatio < "
-                + LetterboxConfiguration.MIN_UNRESIZABLE_ASPECT_RATIO);
+        pw.println("        app is eligible for the size compat mode.  If aspectRatio <= "
+                + LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+        pw.println("        both it and R.dimen.config_fixedOrientationLetterboxAspectRatio will");
+        pw.println("        be ignored and framework implementation will determine aspect ratio.");
         pw.println("      --cornerRadius radius");
         pw.println("        Corners radius for activities in the letterbox mode. If radius < 0,");
         pw.println("        both it and R.integer.config_letterboxActivityCornersRadius will be");
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 74e15cf..46091d8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -115,6 +115,7 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RESIZE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SYNC_ENGINE;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_INSETS;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.policy.WindowManagerPolicy.TRANSIT_ENTER;
@@ -392,6 +393,9 @@
     int mSyncSeqId = 0;
     int mLastSeqIdSentToRelayout = 0;
 
+    /** The last syncId associated with a prepareSync or 0 when no sync is active. */
+    int mPrepareSyncSeqId = 0;
+
     /**
      * {@code true} when the client was still drawing for sync when the sync-set was finished or
      * cancelled. This can happen if the window goes away during a sync. In this situation we need
@@ -1247,7 +1251,7 @@
         mSession.windowAddedLocked();
     }
 
-    boolean updateGlobalScale() {
+    void updateGlobalScale() {
         if (hasCompatScale()) {
             if (mOverrideScale != 1f) {
                 mGlobalScale = mToken.hasSizeCompatBounds()
@@ -1257,11 +1261,10 @@
                 mGlobalScale = mToken.getSizeCompatScale();
             }
             mInvGlobalScale = 1f / mGlobalScale;
-            return true;
+            return;
         }
 
         mGlobalScale = mInvGlobalScale = 1f;
-        return false;
     }
 
     /**
@@ -1514,23 +1517,23 @@
         final boolean dragResizingChanged = isDragResizeChanged()
                 && !isDragResizingChangeReported();
 
+        final boolean attachedFrameChanged = LOCAL_LAYOUT
+                && mLayoutAttached && getParentWindow().frameChanged();
+
         if (DEBUG) {
             Slog.v(TAG_WM, "Resizing " + this + ": configChanged=" + configChanged
                     + " dragResizingChanged=" + dragResizingChanged
                     + " last=" + mWindowFrames.mLastFrame + " frame=" + mWindowFrames.mFrame);
         }
 
-        // We update mLastFrame always rather than in the conditional with the last inset
-        // variables, because mFrameSizeChanged only tracks the width and height changing.
-        updateLastFrames();
-
         // Add a window that is using blastSync to the resizing list if it hasn't been reported
         // already. This because the window is waiting on a finishDrawing from the client.
         if (didFrameInsetsChange
                 || configChanged
                 || insetsChanged
                 || dragResizingChanged
-                || shouldSendRedrawForSync()) {
+                || shouldSendRedrawForSync()
+                || attachedFrameChanged) {
             ProtoLog.v(WM_DEBUG_RESIZE,
                         "Resize reasons for w=%s:  %s configChanged=%b dragResizingChanged=%b",
                         this, mWindowFrames.getInsetsChangedInfo(),
@@ -1586,6 +1589,10 @@
         }
     }
 
+    private boolean frameChanged() {
+        return !mWindowFrames.mFrame.equals(mWindowFrames.mLastFrame);
+    }
+
     boolean getOrientationChanging() {
         // In addition to the local state flag, we must also consider the difference in the last
         // reported configuration vs. the current state. If the client code has not been informed of
@@ -3837,6 +3844,12 @@
         if (mInvGlobalScale != 1.0f && hasCompatScale()) {
             outFrames.displayFrame.scale(mInvGlobalScale);
         }
+        if (mLayoutAttached) {
+            if (outFrames.attachedFrame == null) {
+                outFrames.attachedFrame = new Rect();
+            }
+            outFrames.attachedFrame.set(getParentWindow().getCompatFrame());
+        }
 
         // Note: in the cases where the window is tied to an activity, we should not send a
         // configuration update when the window has requested to be hidden. Doing so can lead to
@@ -3888,6 +3901,10 @@
         mDragResizingChangeReported = true;
         mWindowFrames.clearReportResizeHints();
 
+        // We update mLastFrame always rather than in the conditional with the last inset
+        // variables, because mFrameSizeChanged only tracks the width and height changing.
+        updateLastFrames();
+
         final int prevRotation = mLastReportedConfiguration
                 .getMergedConfiguration().windowConfiguration.getRotation();
         fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,
@@ -4406,6 +4423,8 @@
                 pw.println(prefix + "Requested visibilities: " + visibilityString);
             }
         }
+
+        pw.println(prefix + "mPrepareSyncSeqId=" + mPrepareSyncSeqId);
     }
 
     @Override
@@ -5897,6 +5916,13 @@
         return mWinAnimator.getSurfaceControl();
     }
 
+    /** Drops a buffer for this window's view-root from a transaction */
+    private void dropBufferFrom(Transaction t) {
+        SurfaceControl viewSurface = getClientViewRootSurface();
+        if (viewSurface == null) return;
+        t.setBuffer(viewSurface, (android.hardware.HardwareBuffer) null);
+    }
+
     @Override
     boolean prepareSync() {
         if (!mDrawHandlers.isEmpty()) {
@@ -5912,7 +5938,18 @@
         // to draw even if the children draw first or don't need to sync, so we start
         // in WAITING state rather than READY.
         mSyncState = SYNC_STATE_WAITING_FOR_DRAW;
+
+        if (mPrepareSyncSeqId > 0) {
+            // another prepareSync during existing sync (eg. reparented), so pre-emptively
+            // drop buffer (if exists). If the buffer hasn't been received yet, it will be
+            // dropped in finishDrawing.
+            ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Preparing to sync a window that was already in the"
+                            + " sync, so try dropping buffer. win=%s", this);
+            dropBufferFrom(mSyncTransaction);
+        }
+
         mSyncSeqId++;
+        mPrepareSyncSeqId = mSyncSeqId;
         requestRedrawForSync();
         return true;
     }
@@ -5933,6 +5970,13 @@
         if (mSyncState == SYNC_STATE_WAITING_FOR_DRAW && mRedrawForSyncReported) {
             mClientWasDrawingForSync = true;
         }
+        mPrepareSyncSeqId = 0;
+        if (cancel) {
+            // This is leaving sync so any buffers left in the sync have a chance of
+            // being applied out-of-order and can also block the buffer queue for this
+            // window. To prevent this, drop the buffer.
+            dropBufferFrom(mSyncTransaction);
+        }
         super.finishSync(outMergedTransaction, cancel);
     }
 
@@ -5954,6 +5998,17 @@
                     .notifyStartingWindowDrawn(mActivityRecord);
         }
 
+        final boolean syncActive = mPrepareSyncSeqId > 0;
+        final boolean syncStillPending = syncActive && mPrepareSyncSeqId > syncSeqId;
+        if (syncStillPending && postDrawTransaction != null) {
+            ProtoLog.d(WM_DEBUG_SYNC_ENGINE, "Got a buffer for request id=%d but latest request is"
+                    + " id=%d. Since the buffer is out-of-date, drop it. win=%s", syncSeqId,
+                    mPrepareSyncSeqId, this);
+            // sync is waiting for a newer seqId, so this buffer is obsolete and can be dropped
+            // to free up the buffer queue.
+            dropBufferFrom(postDrawTransaction);
+        }
+
         final boolean hasSyncHandlers = executeDrawHandlers(postDrawTransaction, syncSeqId);
 
         boolean skipLayout = false;
@@ -5966,10 +6021,15 @@
             // Layout is not needed because the window will be hidden by the fade leash.
             postDrawTransaction = null;
             skipLayout = true;
-        } else if (onSyncFinishedDrawing() && postDrawTransaction != null) {
-            mSyncTransaction.merge(postDrawTransaction);
-            // Consume the transaction because the sync group will merge it.
-            postDrawTransaction = null;
+        } else if (syncActive) {
+            if (!syncStillPending) {
+                onSyncFinishedDrawing();
+            }
+            if (postDrawTransaction != null) {
+                mSyncTransaction.merge(postDrawTransaction);
+                // Consume the transaction because the sync group will merge it.
+                postDrawTransaction = null;
+            }
         }
 
         final boolean layoutNeeded =
@@ -6199,4 +6259,9 @@
                           @WindowTraceLogLevel int logLevel) {
         dumpDebug(proto, fieldId, logLevel);
     }
+
+    public boolean cancelAndRedraw() {
+        // Cancel any draw requests during a sync.
+        return mPrepareSyncSeqId > 0;
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 05ea9cc..06fb4b0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1982,6 +1982,10 @@
         synchronized (getLockObject()) {
             mOwners.load();
             setDeviceOwnershipSystemPropertyLocked();
+            if (mOwners.hasDeviceOwner()) {
+                setGlobalSettingDeviceOwnerType(
+                        mOwners.getDeviceOwnerType(mOwners.getDeviceOwnerPackageName()));
+            }
         }
     }
 
@@ -8811,6 +8815,7 @@
         deleteTransferOwnershipBundleLocked(userId);
         toggleBackupServiceActive(UserHandle.USER_SYSTEM, true);
         pushUserControlDisabledPackagesLocked(userId);
+        setGlobalSettingDeviceOwnerType(DEVICE_OWNER_TYPE_DEFAULT);
     }
 
     private void clearApplicationRestrictions(int userId) {
@@ -18377,6 +18382,14 @@
                 "Test only admins can only set the device owner type more than once");
 
         mOwners.setDeviceOwnerType(packageName, deviceOwnerType, isAdminTestOnly);
+        setGlobalSettingDeviceOwnerType(deviceOwnerType);
+    }
+
+    // TODO(b/237065504): Allow mainline modules to get the device owner type. This is a workaround
+    // to get the device owner type in PermissionController. See HibernationPolicy.kt.
+    private void setGlobalSettingDeviceOwnerType(int deviceOwnerType) {
+        mInjector.binderWithCleanCallingIdentity(
+                () -> mInjector.settingsGlobalPutInt("device_owner_type", deviceOwnerType));
     }
 
     @Override
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ef311c2..66c9f55 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -412,7 +412,7 @@
             "/apex/com.android.uwb/javalib/service-uwb.jar";
     private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService";
     private static final String BLUETOOTH_APEX_SERVICE_JAR_PATH =
-            "/apex/com.android.bluetooth/javalib/service-bluetooth.jar";
+            "/apex/com.android.btservices/javalib/service-bluetooth.jar";
     private static final String BLUETOOTH_SERVICE_CLASS =
             "com.android.server.bluetooth.BluetoothService";
     private static final String SAFETY_CENTER_SERVICE_CLASS =
diff --git a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
index b36aa06..e87dd4b 100644
--- a/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/adb/AdbDebuggingManagerTest.java
@@ -36,8 +36,6 @@
 
 import androidx.test.InstrumentationRegistry;
 
-import com.android.server.FgThread;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,6 +46,11 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.FileReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
@@ -88,6 +91,7 @@
     private long mOriginalAllowedConnectionTime;
     private File mAdbKeyXmlFile;
     private File mAdbKeyFile;
+    private FakeTicker mFakeTicker;
 
     @Before
     public void setUp() throws Exception {
@@ -96,14 +100,25 @@
         if (mAdbKeyFile.exists()) {
             mAdbKeyFile.delete();
         }
-        mManager = new AdbDebuggingManager(mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile);
         mAdbKeyXmlFile = new File(mContext.getFilesDir(), "test_adb_keys.xml");
         if (mAdbKeyXmlFile.exists()) {
             mAdbKeyXmlFile.delete();
         }
+
+        mFakeTicker = new FakeTicker();
+        // Set the ticker time to October 22, 2008 (the day the T-Mobile G1 was released)
+        mFakeTicker.advance(1224658800L);
+
         mThread = new AdbDebuggingThreadTest();
-        mKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
-        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper(), mThread, mKeyStore);
+        mManager = new AdbDebuggingManager(
+                mContext, ADB_CONFIRM_COMPONENT, mAdbKeyFile, mAdbKeyXmlFile, mThread, mFakeTicker);
+
+        mHandler = mManager.mHandler;
+        mThread.setHandler(mHandler);
+
+        mHandler.initKeyStore();
+        mKeyStore = mHandler.mAdbKeyStore;
+
         mOriginalAllowedConnectionTime = mKeyStore.getAllowedConnectionTime();
         mBlockingQueue = new ArrayBlockingQueue<>(1);
     }
@@ -122,7 +137,7 @@
     private void setAllowedConnectionTime(long connectionTime) {
         Settings.Global.putLong(mContext.getContentResolver(),
                 Settings.Global.ADB_ALLOWED_CONNECTION_TIME, connectionTime);
-    };
+    }
 
     @Test
     public void testAllowNewKeyOnce() throws Exception {
@@ -158,20 +173,15 @@
         // Allow a connection from a new key with the 'Always allow' option selected.
         runAdbTest(TEST_KEY_1, true, true, false);
 
-        // Get the last connection time for the currently connected key to verify that it is updated
-        // after the disconnect.
-        long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-
-        // Sleep for a small amount of time to ensure a difference can be observed in the last
-        // connection time after a disconnect.
-        Thread.sleep(10);
+        // Advance the clock by 10ms to ensure there's a difference
+        mFakeTicker.advance(10 * 1_000_000);
 
         // Send the disconnect message for the currently connected key to trigger an update of the
         // last connection time.
         disconnectKey(TEST_KEY_1);
-        assertNotEquals(
+        assertEquals(
                 "The last connection time was not updated after the disconnect",
-                lastConnectionTime,
+                mFakeTicker.currentTimeMillis(),
                 mKeyStore.getLastConnectionTime(TEST_KEY_1));
     }
 
@@ -244,8 +254,8 @@
         // Get the current last connection time for comparison after the scheduled job is run
         long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
 
-        // Sleep a small amount of time to ensure that the updated connection time changes
-        Thread.sleep(10);
+        // Advance a small amount of time to ensure that the updated connection time changes
+        mFakeTicker.advance(10);
 
         // Send a message to the handler to update the last connection time for the active key
         updateKeyStore();
@@ -269,13 +279,13 @@
         persistKeyStore();
         assertTrue(
                 "The key with the 'Always allow' option selected was not persisted in the keystore",
-                mManager.new AdbKeyStore(mAdbKeyXmlFile).isKeyAuthorized(TEST_KEY_1));
+                mManager.new AdbKeyStore().isKeyAuthorized(TEST_KEY_1));
 
         // Get the current last connection time to ensure it is updated in the persisted keystore.
         long lastConnectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
 
-        // Sleep a small amount of time to ensure the last connection time is updated.
-        Thread.sleep(10);
+        // Advance a small amount of time to ensure the last connection time is updated.
+        mFakeTicker.advance(10);
 
         // Send a message to the handler to update the last connection time for the active key.
         updateKeyStore();
@@ -286,7 +296,7 @@
         assertNotEquals(
                 "The last connection time in the key file was not updated after the update "
                         + "connection time message", lastConnectionTime,
-                mManager.new AdbKeyStore(mAdbKeyXmlFile).getLastConnectionTime(TEST_KEY_1));
+                mManager.new AdbKeyStore().getLastConnectionTime(TEST_KEY_1));
         // Verify that the key is in the adb_keys file
         assertTrue("The key was not in the adb_keys file after persisting the keystore",
                 isKeyInFile(TEST_KEY_1, mAdbKeyFile));
@@ -327,8 +337,8 @@
         // Set the allowed window to a small value to ensure the time is beyond the allowed window.
         setAllowedConnectionTime(1);
 
-        // Sleep for a small amount of time to exceed the allowed window.
-        Thread.sleep(10);
+        // Advance a small amount of time to exceed the allowed window.
+        mFakeTicker.advance(10);
 
         // The AdbKeyStore has a method to get the time of the next key expiration to ensure the
         // scheduled job runs at the time of the next expiration or after 24 hours, whichever occurs
@@ -478,9 +488,12 @@
         // Set the current expiration time to a minute from expiration and verify this new value is
         // returned.
         final long newExpirationTime = 60000;
-        mKeyStore.setLastConnectionTime(TEST_KEY_1,
-                System.currentTimeMillis() - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
-                        + newExpirationTime, true);
+        mKeyStore.setLastConnectionTime(
+                TEST_KEY_1,
+                mFakeTicker.currentTimeMillis()
+                        - Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME
+                        + newExpirationTime,
+                true);
         expirationTime = mKeyStore.getNextExpirationTime();
         if (Math.abs(expirationTime - newExpirationTime) > epsilon) {
             fail("The expiration time for a key about to expire, " + expirationTime
@@ -525,7 +538,7 @@
         // Get the last connection time for the key to verify that it is updated when the connected
         // key message is sent.
         long connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         mHandler.obtainMessage(AdbDebuggingManager.AdbDebuggingHandler.MESSAGE_ADB_CONNECTED_KEY,
                 TEST_KEY_1).sendToTarget();
         flushHandlerQueue();
@@ -536,7 +549,7 @@
 
         // Verify that the scheduled job updates the connection time of the key.
         connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for the key must be updated when the update keystore message"
@@ -545,7 +558,7 @@
 
         // Verify that the connection time is updated when the key is disconnected.
         connectionTime = mKeyStore.getLastConnectionTime(TEST_KEY_1);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         disconnectKey(TEST_KEY_1);
         assertNotEquals(
                 "The connection time for the key must be updated when the disconnected message is"
@@ -628,11 +641,11 @@
         setAllowedConnectionTime(Settings.Global.DEFAULT_ADB_ALLOWED_CONNECTION_TIME);
 
         // The untracked keys should be added to the keystore as part of the constructor.
-        AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore(mAdbKeyXmlFile);
+        AdbDebuggingManager.AdbKeyStore adbKeyStore = mManager.new AdbKeyStore();
 
         // Verify that the connection time for each test key is within a small value of the current
         // time.
-        long time = System.currentTimeMillis();
+        long time = mFakeTicker.currentTimeMillis();
         for (String key : testKeys) {
             long connectionTime = adbKeyStore.getLastConnectionTime(key);
             if (Math.abs(time - connectionTime) > epsilon) {
@@ -651,11 +664,11 @@
         runAdbTest(TEST_KEY_1, true, true, false);
         runAdbTest(TEST_KEY_2, true, true, false);
 
-        // Sleep a small amount of time to ensure the connection time is updated by the scheduled
+        // Advance a small amount of time to ensure the connection time is updated by the scheduled
         // job.
         long connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
         long connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for test key 1 must be updated after the scheduled job runs",
@@ -669,7 +682,7 @@
         disconnectKey(TEST_KEY_2);
         connectionTime1 = mKeyStore.getLastConnectionTime(TEST_KEY_1);
         connectionTime2 = mKeyStore.getLastConnectionTime(TEST_KEY_2);
-        Thread.sleep(10);
+        mFakeTicker.advance(10);
         updateKeyStore();
         assertNotEquals(
                 "The connection time for test key 1 must be updated after another key is "
@@ -686,8 +699,6 @@
         // to clear the adb authorizations when adb is disabled after a boot a NullPointerException
         // was thrown as deleteKeyStore is invoked against the key store. This test ensures the
         // key store can be successfully cleared when adb is disabled.
-        mHandler = mManager.new AdbDebuggingHandler(FgThread.get().getLooper());
-
         clearKeyStore();
     }
 
@@ -723,6 +734,9 @@
 
         // Now remove one of the keys and make sure the other key is still there
         mKeyStore.removeKey(TEST_KEY_1);
+        // Wait for the handler queue to receive the MESSAGE_ADB_PERSIST_KEYSTORE
+        flushHandlerQueue();
+
         assertFalse("The key was still in the adb_keys file after removing the key",
                 isKeyInFile(TEST_KEY_1, mAdbKeyFile));
         assertTrue("The key was not in the adb_keys file after removing a different key",
@@ -730,6 +744,95 @@
     }
 
     @Test
+    public void testAdbKeyStore_addDuplicateKey_doesNotAddDuplicateToAdbKeyFile() throws Exception {
+        setAllowedConnectionTime(0);
+
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        assertEquals("adb_keys contains duplicate keys", 1, adbKeyFileKeys(mAdbKeyFile).size());
+    }
+
+    @Test
+    public void testAdbKeyStore_adbTempKeysFile_readsLastConnectionTimeFromXml() throws Exception {
+        long insertTime = mFakeTicker.currentTimeMillis();
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        mFakeTicker.advance(10);
+        AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore();
+
+        assertEquals(
+                "KeyStore not populated from the XML file.",
+                insertTime,
+                newKeyStore.getLastConnectionTime(TEST_KEY_1));
+    }
+
+    @Test
+    public void test_notifyKeyFilesUpdated_filesDeletedRemovesPreviouslyAddedKey()
+            throws Exception {
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        Files.delete(mAdbKeyXmlFile.toPath());
+        Files.delete(mAdbKeyFile.toPath());
+
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        assertFalse(
+                "Key is authorized after reloading deleted key files. Was state preserved?",
+                mKeyStore.isKeyAuthorized(TEST_KEY_1));
+    }
+
+    @Test
+    public void test_notifyKeyFilesUpdated_newKeyIsAuthorized() throws Exception {
+        runAdbTest(TEST_KEY_1, true, true, false);
+        persistKeyStore();
+
+        // Back up the existing key files
+        Path tempXmlFile = Files.createTempFile("adbKeyXmlFile", ".tmp");
+        Path tempAdbKeysFile = Files.createTempFile("adb_keys", ".tmp");
+        Files.copy(mAdbKeyXmlFile.toPath(), tempXmlFile, StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(mAdbKeyFile.toPath(), tempAdbKeysFile, StandardCopyOption.REPLACE_EXISTING);
+
+        // Delete the existing key files
+        Files.delete(mAdbKeyXmlFile.toPath());
+        Files.delete(mAdbKeyFile.toPath());
+
+        // Notify the manager that adb key files have changed.
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        // Copy the files back
+        Files.copy(tempXmlFile, mAdbKeyXmlFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(tempAdbKeysFile, mAdbKeyFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+
+        // Tell the manager that the key files have changed.
+        mManager.notifyKeyFilesUpdated();
+        flushHandlerQueue();
+
+        assertTrue(
+                "Key is not authorized after reloading key files.",
+                mKeyStore.isKeyAuthorized(TEST_KEY_1));
+    }
+
+    @Test
+    public void testAdbKeyStore_adbWifiConnect_storesBssidWhenAlwaysAllow() throws Exception {
+        String trustedNetwork = "My Network";
+        mKeyStore.addTrustedNetwork(trustedNetwork);
+        persistKeyStore();
+
+        AdbDebuggingManager.AdbKeyStore newKeyStore = mManager.new AdbKeyStore();
+
+        assertTrue(
+                "Persisted trusted network not found in new keystore instance.",
+                newKeyStore.isTrustedNetwork(trustedNetwork));
+    }
+
+    @Test
     public void testIsValidMdnsServiceName() {
         // Longer than 15 characters
         assertFalse(isValidMdnsServiceName("abcd1234abcd1234"));
@@ -1030,28 +1133,27 @@
         if (key == null) {
             return false;
         }
+        return adbKeyFileKeys(keyFile).contains(key);
+    }
+
+    private static List<String> adbKeyFileKeys(File keyFile) throws Exception {
+        List<String> keys = new ArrayList<>();
         if (keyFile.exists()) {
             try (BufferedReader in = new BufferedReader(new FileReader(keyFile))) {
                 String currKey;
                 while ((currKey = in.readLine()) != null) {
-                    if (key.equals(currKey)) {
-                        return true;
-                    }
+                    keys.add(currKey);
                 }
             }
         }
-        return false;
+        return keys;
     }
 
     /**
      * Helper class that extends AdbDebuggingThread to receive the response from AdbDebuggingManager
      * indicating whether the key should be allowed to  connect.
      */
-    class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
-        AdbDebuggingThreadTest() {
-            mManager.super();
-        }
-
+    private class AdbDebuggingThreadTest extends AdbDebuggingManager.AdbDebuggingThread {
         @Override
         public void sendResponse(String msg) {
             TestResult result = new TestResult(TestResult.RESULT_RESPONSE_RECEIVED, msg);
@@ -1091,4 +1193,17 @@
             return "{mReturnCode = " + mReturnCode + ", mMessage = " + mMessage + "}";
         }
     }
+
+    private static class FakeTicker implements AdbDebuggingManager.Ticker {
+        private long mCurrentTime;
+
+        private void advance(long milliseconds) {
+            mCurrentTime += milliseconds;
+        }
+
+        @Override
+        public long currentTimeMillis() {
+            return mCurrentTime;
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
index 25cf8a8..e95924a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java
@@ -20,7 +20,9 @@
 import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
 import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGATIVE;
 
-import static com.android.server.biometrics.BiometricServiceStateProto.*;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED;
+import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
@@ -32,6 +34,8 @@
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -280,6 +284,43 @@
     }
 
     @Test
+    public void testOnDialogAnimatedInDoesNothingDuringInvalidState() throws Exception {
+        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_UDFPS_OPTICAL);
+        final long operationId = 123;
+        final int userId = 10;
+
+        final AuthSession session = createAuthSession(mSensors,
+                false /* checkDevicePolicyManager */,
+                Authenticators.BIOMETRIC_STRONG,
+                TEST_REQUEST_ID,
+                operationId,
+                userId);
+        final IBiometricAuthenticator impl = session.mPreAuthInfo.eligibleSensors.get(0).impl;
+
+        session.goToInitialState();
+        for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) {
+            assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState());
+            session.onCookieReceived(
+                    session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie());
+        }
+        assertTrue(session.allCookiesReceived());
+        assertEquals(STATE_AUTH_STARTED, session.getState());
+        verify(impl, never()).startPreparedClient(anyInt());
+
+        // First invocation should start the client monitor.
+        session.onDialogAnimatedIn();
+        assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
+        verify(impl).startPreparedClient(anyInt());
+
+        // Subsequent invocations should not start the client monitor again.
+        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn();
+        session.onDialogAnimatedIn();
+        assertEquals(STATE_AUTH_STARTED_UI_SHOWING, session.getState());
+        verify(impl, times(1)).startPreparedClient(anyInt());
+    }
+
+    @Test
     public void testCancelAuthentication_whenStateAuthCalled_invokesCancel()
             throws RemoteException {
         testInvokesCancel(session -> session.onCancelAuthSession(false /* force */));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index c173473..9e9d703 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -80,11 +80,14 @@
 
     private Handler mHandler;
     private BiometricSchedulerOperation mOperation;
+    private boolean mIsDebuggable;
 
     @Before
     public void setUp() {
         mHandler = new Handler(TestableLooper.get(this).getLooper());
-        mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback);
+        mIsDebuggable = false;
+        mOperation = new BiometricSchedulerOperation(mClientMonitor, mClientCallback,
+                () -> mIsDebuggable);
     }
 
     @Test
@@ -126,6 +129,34 @@
     }
 
     @Test
+    public void testSecondStartWithCookieCrashesWhenDebuggable() {
+        final int cookie = 5;
+        mIsDebuggable = true;
+        when(mClientMonitor.getCookie()).thenReturn(cookie);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(started).isTrue();
+
+        assertThrows(IllegalStateException.class,
+                () -> mOperation.startWithCookie(mOnStartCallback, cookie));
+    }
+
+    @Test
+    public void testSecondStartWithCookieFailsNicelyWhenNotDebuggable() {
+        final int cookie = 5;
+        mIsDebuggable = false;
+        when(mClientMonitor.getCookie()).thenReturn(cookie);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(started).isTrue();
+
+        final boolean startedAgain = mOperation.startWithCookie(mOnStartCallback, cookie);
+        assertThat(startedAgain).isFalse();
+    }
+
+    @Test
     public void startsWhenReadyAndHalAvailable() {
         when(mClientMonitor.getCookie()).thenReturn(0);
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
@@ -170,7 +201,34 @@
     }
 
     @Test
+    public void secondStartCrashesWhenDebuggable() {
+        mIsDebuggable = true;
+        when(mClientMonitor.getCookie()).thenReturn(0);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.start(mOnStartCallback);
+        assertThat(started).isTrue();
+
+        assertThrows(IllegalStateException.class, () -> mOperation.start(mOnStartCallback));
+    }
+
+    @Test
+    public void secondStartFailsNicelyWhenNotDebuggable() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getCookie()).thenReturn(0);
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        final boolean started = mOperation.start(mOnStartCallback);
+        assertThat(started).isTrue();
+
+        final boolean startedAgain = mOperation.start(mOnStartCallback);
+        assertThat(startedAgain).isFalse();
+    }
+
+    @Test
     public void doesNotStartWithCookie() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getCookie()).thenReturn(9);
         assertThrows(IllegalStateException.class,
                 () -> mOperation.start(mock(ClientMonitorCallback.class)));
@@ -178,6 +236,8 @@
 
     @Test
     public void cannotRestart() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.start(mOnStartCallback);
@@ -188,6 +248,8 @@
 
     @Test
     public void abortsNotRunning() {
+        // This class only throws exceptions when debuggable.
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.abort();
@@ -200,7 +262,8 @@
     }
 
     @Test
-    public void cannotAbortRunning() {
+    public void abortCrashesWhenDebuggableIfOperationIsRunning() {
+        mIsDebuggable = true;
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
         mOperation.start(mOnStartCallback);
@@ -209,6 +272,16 @@
     }
 
     @Test
+    public void abortFailsNicelyWhenNotDebuggableIfOperationIsRunning() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.start(mOnStartCallback);
+
+        mOperation.abort();
+    }
+
+    @Test
     public void cancel() {
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
@@ -254,6 +327,30 @@
     }
 
     @Test
+    public void cancelCrashesWhenDebuggableIfOperationIsFinished() {
+        mIsDebuggable = true;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.abort();
+        assertThat(mOperation.isFinished()).isTrue();
+
+        final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
+        assertThrows(IllegalStateException.class, () -> mOperation.cancel(mHandler, cancelCb));
+    }
+
+    @Test
+    public void cancelFailsNicelyWhenNotDebuggableIfOperationIsFinished() {
+        mIsDebuggable = false;
+        when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
+
+        mOperation.abort();
+        assertThat(mOperation.isFinished()).isTrue();
+
+        final ClientMonitorCallback cancelCb = mock(ClientMonitorCallback.class);
+        mOperation.cancel(mHandler, cancelCb);
+    }
+
+    @Test
     public void markCanceling() {
         when(mClientMonitor.getFreshDaemon()).thenReturn(mHal);
 
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 f242fda..c80547c 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
@@ -213,7 +213,7 @@
                 mContext.getSystemService(WindowManager.class), threadVerifier);
 
         mAssociationInfo = new AssociationInfo(1, 0, null,
-                MacAddress.BROADCAST_ADDRESS, "", null, true, false, 0, 0);
+                MacAddress.BROADCAST_ADDRESS, "", null, true, false, false, 0, 0);
 
         VirtualDeviceParams params = new VirtualDeviceParams
                 .Builder()
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
index 0ed90d2..6a6cd6c 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
@@ -16,13 +16,11 @@
 
 package com.android.server.display;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -34,17 +32,18 @@
 import android.os.IThermalService;
 import android.os.Message;
 import android.os.PowerManager;
-import android.os.Temperature.ThrottlingStatus;
 import android.os.Temperature;
+import android.os.Temperature.ThrottlingStatus;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.os.BackgroundThread;
 import com.android.server.display.BrightnessThrottler.Injector;
-import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData;
+import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -55,7 +54,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 @SmallTest
@@ -70,6 +68,8 @@
     @Mock IThermalService mThermalServiceMock;
     @Mock Injector mInjectorMock;
 
+    DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake;
+
     @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor;
 
     @Before
@@ -83,6 +83,8 @@
                 return true;
             }
         });
+        mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig();
+        when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake);
 
     }
 
@@ -292,6 +294,170 @@
         assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
     }
 
+    @Test public void testUpdateThrottlingData() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are overridden by setting the data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.4");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.4f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Update thresholds
+        // This data is equivalent to the string "123,1,critical,0.8", passed below
+        final ThrottlingLevel newLevel = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.8f);
+        // Set new (valid) data from device config
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.8");
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(newLevel.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    @Test public void testInvalidThrottlingStrings() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are not overridden by invalid data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // None of these are valid so shouldn't override the original data
+        mDeviceConfigFake.setBrightnessThrottlingData("321,1,critical,0.4");  // Not the current id
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,0,critical,0.4");  // Incorrect number
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,2,critical,0.4");  // Incorrect number
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,invalid,0.4");   // Invalid level
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,none"); // Invalid brightness
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,-3");   // Invalid brightness
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("invalid string");      // Invalid format
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("");                    // Invalid format
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+    }
+
+    private void testThrottling(BrightnessThrottler throttler, IThermalEventListener listener,
+            float tooLowCap, float tooHighCap) throws Exception {
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                tooHighCap);
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    @Test public void testMultipleConfigPoints() throws Exception {
+        // Initialise brightness throttling levels
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+
+        // These are identical to the string set below
+        final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE,
+                0.9f);
+        final ThrottlingLevel levelCritical = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_CRITICAL, 0.5f);
+        final ThrottlingLevel levelEmergency = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f);
+
+        mDeviceConfigFake.setBrightnessThrottlingData(
+                "123,3,severe,0.9,critical,0.5,emergency,0.1");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Ensure that the multiple levels set via the string through the device config correctly
+        // override the original display device config ones.
+
+        // levelSevere
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // levelCritical
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        //levelEmergency
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.1f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
     private void assertThrottlingLevelsEquals(
             List<ThrottlingLevel> expected,
             List<ThrottlingLevel> actual) {
@@ -307,12 +473,13 @@
     }
 
     private BrightnessThrottler createThrottlerUnsupported() {
-        return new BrightnessThrottler(mInjectorMock, mHandler, null, () -> {});
+        return new BrightnessThrottler(mInjectorMock, mHandler, mHandler, null, () -> {}, null);
     }
 
     private BrightnessThrottler createThrottlerSupported(BrightnessThrottlingData data) {
         assertNotNull(data);
-        return new BrightnessThrottler(mInjectorMock, mHandler, data, () -> {});
+        return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(),
+                data, () -> {}, "123");
     }
 
     private Temperature getSkinTemp(@ThrottlingStatus int status) {
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 864f315..968e1d8 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static android.hardware.display.DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS;
@@ -1902,6 +1903,11 @@
                     KEY_REFRESH_RATE_IN_HBM_HDR, String.valueOf(fps));
         }
 
+        void setBrightnessThrottlingData(String brightnessThrottlingData) {
+            putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    KEY_BRIGHTNESS_THROTTLING_DATA, brightnessThrottlingData);
+        }
+
         void setLowDisplayBrightnessThresholds(int[] brightnessThresholds) {
             String thresholds = toPropertyValue(brightnessThresholds);
 
diff --git a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java
index 363c26b..bbed1b6 100644
--- a/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/color/ColorDisplayServiceTest.java
@@ -16,11 +16,14 @@
 
 package com.android.server.display.color;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -1130,6 +1133,15 @@
                 eq(ColorDisplayManager.COLOR_MODE_BOOSTED), any(), eq(Display.COLOR_MODE_INVALID));
     }
 
+    @Test
+    public void getColorMode_noAvailableModes_returnsNotSet() {
+        when(mResourcesSpy.getIntArray(R.array.config_availableColorModes))
+                .thenReturn(new int[] {});
+        startService();
+        verify(mDisplayTransformManager, never()).setColorMode(anyInt(), any(), anyInt());
+        assertThat(mBinderService.getColorMode()).isEqualTo(-1);
+    }
+
     /**
      * Configures Night display to use a custom schedule.
      *
diff --git a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
index 75bd2cc..bc2c57e 100644
--- a/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
+++ b/services/tests/servicestests/src/com/android/server/usage/UsageStatsDatabaseTest.java
@@ -22,6 +22,7 @@
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
 
 import android.app.usage.TimeSparseArray;
 import android.app.usage.UsageEvents.Event;
@@ -440,6 +441,7 @@
         prevDB.readMappingsLocked();
         prevDB.init(1);
         prevDB.putUsageStats(UsageStatsManager.INTERVAL_DAILY, mIntervalStats);
+        Set<String> prevDBApps = mIntervalStats.packageStats.keySet();
         // Create a backup with a specific version
         byte[] blob = prevDB.getBackupPayload(KEY_USAGE_STATS, version);
         if (version >= 1 && version <= 3) {
@@ -447,6 +449,11 @@
                     "UsageStatsDatabase shouldn't be able to write backups as XML");
             return;
         }
+        if (version < 1 || version > UsageStatsDatabase.BACKUP_VERSION) {
+            assertFalse(blob != null && blob.length != 0,
+                    "UsageStatsDatabase shouldn't be able to write backups for unknown versions");
+            return;
+        }
 
         clearUsageStatsFiles();
 
@@ -454,9 +461,11 @@
         newDB.readMappingsLocked();
         newDB.init(1);
         // Attempt to restore the usage stats from the backup
-        newDB.applyRestoredPayload(KEY_USAGE_STATS, blob);
-        List<IntervalStats> stats = newDB.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, 0, mEndTime,
-                mIntervalStatsVerifier);
+        Set<String> restoredApps = newDB.applyRestoredPayload(KEY_USAGE_STATS, blob);
+        assertTrue(restoredApps.containsAll(prevDBApps),
+                "List of restored apps does not match list backed-up apps list.");
+        List<IntervalStats> stats = newDB.queryUsageStats(
+                UsageStatsManager.INTERVAL_DAILY, 0, mEndTime, mIntervalStatsVerifier);
 
         if (version > UsageStatsDatabase.BACKUP_VERSION || version < 1) {
             assertFalse(stats != null && !stats.isEmpty(),
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index c735bb7..8a96feb 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -778,8 +778,7 @@
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
-                HAPTIC_FEEDBACK_ATTRS);
+        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK), HAPTIC_FEEDBACK_ATTRS);
 
         // Wait before checking it never played a second effect.
         assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
@@ -793,49 +792,78 @@
     }
 
     @Test
-    public void vibrate_withOngoingAlarmVibration_ignoresEffect() throws Exception {
+    public void vibrate_withNewRepeatingVibration_cancelsOngoingEffect() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         VibratorManagerService service = createSystemReadyService();
 
         VibrationEffect alarmEffect = VibrationEffect.createWaveform(
                 new long[]{10_000, 10_000}, new int[]{128, 255}, -1);
-        vibrate(service, alarmEffect, new VibrationAttributes.Builder().setUsage(
-                VibrationAttributes.USAGE_ALARM).build());
+        vibrate(service, alarmEffect, ALARM_ATTRS);
 
         // VibrationThread will start this vibration async, so wait before checking it started.
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
-                HAPTIC_FEEDBACK_ATTRS);
+        VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
+                new long[]{10_000, 10_000}, new int[]{128, 255}, 1);
+        vibrate(service, repeatingEffect, NOTIFICATION_ATTRS);
 
-        // Wait before checking it never played a second effect.
-        assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
-                service, /* timeout= */ 50));
+        // VibrationThread will start this vibration async, so wait before checking it started.
+        assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
+                service, TEST_TIMEOUT_MILLIS));
+
+        // The second vibration should have recorded that the vibrators were turned on.
+        verify(mBatteryStatsMock, times(2)).noteVibratorOn(anyInt(), anyLong());
     }
 
     @Test
-    public void vibrate_withOngoingRingtoneVibration_ignoresEffect() throws Exception {
+    public void vibrate_withOngoingHigherImportanceVibration_ignoresEffect() throws Exception {
         mockVibrators(1);
         mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
         VibratorManagerService service = createSystemReadyService();
 
-        VibrationEffect alarmEffect = VibrationEffect.createWaveform(
+        VibrationEffect effect = VibrationEffect.createWaveform(
                 new long[]{10_000, 10_000}, new int[]{128, 255}, -1);
-        vibrate(service, alarmEffect, new VibrationAttributes.Builder().setUsage(
-                VibrationAttributes.USAGE_RINGTONE).build());
+        vibrate(service, effect, ALARM_ATTRS);
 
         // VibrationThread will start this vibration async, so wait before checking it started.
         assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
                 service, TEST_TIMEOUT_MILLIS));
 
-        vibrate(service, VibrationEffect.get(VibrationEffect.EFFECT_CLICK),
-                HAPTIC_FEEDBACK_ATTRS);
+        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
 
         // Wait before checking it never played a second effect.
         assertFalse(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
                 service, /* timeout= */ 50));
+
+        // The second vibration shouldn't have recorded that the vibrators were turned on.
+        verify(mBatteryStatsMock, times(1)).noteVibratorOn(anyInt(), anyLong());
+    }
+
+    @Test
+    public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect()
+            throws Exception {
+        mockVibrators(1);
+        mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+        VibratorManagerService service = createSystemReadyService();
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                new long[]{10_000, 10_000}, new int[]{128, 255}, -1);
+        vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait before checking it started.
+        assertTrue(waitUntil(s -> !mVibratorProviders.get(1).getAllEffectSegments().isEmpty(),
+                service, TEST_TIMEOUT_MILLIS));
+
+        vibrate(service, effect, RINGTONE_ATTRS);
+
+        // VibrationThread will start this vibration async, so wait before checking it started.
+        assertTrue(waitUntil(s -> mVibratorProviders.get(1).getAllEffectSegments().size() > 1,
+                service, TEST_TIMEOUT_MILLIS));
+
+        // The second vibration should have recorded that the vibrators were turned on.
+        verify(mBatteryStatsMock, times(2)).noteVibratorOn(anyInt(), anyLong());
     }
 
     @Test
@@ -1052,15 +1080,15 @@
                 IVibrator.CAP_COMPOSE_EFFECTS);
         VibratorManagerService service = createSystemReadyService();
 
-        vibrate(service, CombinedVibration.startSequential()
-                .addNext(1, VibrationEffect.createOneShot(100, 125))
-                .combine(), NOTIFICATION_ATTRS);
-        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1,
-                service, TEST_TIMEOUT_MILLIS));
-
         vibrate(service, VibrationEffect.startComposition()
                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
                 .compose(), HAPTIC_FEEDBACK_ATTRS);
+        assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 1,
+                service, TEST_TIMEOUT_MILLIS));
+
+        vibrate(service, CombinedVibration.startSequential()
+                .addNext(1, VibrationEffect.createOneShot(100, 125))
+                .combine(), NOTIFICATION_ATTRS);
         assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 2,
                 service, TEST_TIMEOUT_MILLIS));
 
@@ -1070,25 +1098,25 @@
         assertTrue(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() == 3,
                 service, TEST_TIMEOUT_MILLIS));
 
+        // Ring vibrations have intensity OFF and are not played.
         vibrate(service, VibrationEffect.createOneShot(100, 125), RINGTONE_ATTRS);
         assertFalse(waitUntil(s -> fakeVibrator.getAllEffectSegments().size() > 3,
-                service, TEST_TIMEOUT_MILLIS));
+                service, /* timeout= */ 50));
 
+        // Only 3 effects played successfully.
         assertEquals(3, fakeVibrator.getAllEffectSegments().size());
 
+        // Haptic feedback vibrations will be scaled with SCALE_LOW or none if default is low.
+        assertEquals(defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW,
+                0.5 > ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(0)).getScale());
+
         // Notification vibrations will be scaled with SCALE_HIGH or none if default is high.
         assertEquals(defaultNotificationIntensity < Vibrator.VIBRATION_INTENSITY_HIGH,
                 0.6 < fakeVibrator.getAmplitudes().get(0));
 
-        // Haptic feedback vibrations will be scaled with SCALE_LOW or none if default is low.
-        assertEquals(defaultTouchIntensity > Vibrator.VIBRATION_INTENSITY_LOW,
-                0.5 > ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(1)).getScale());
-
         // Alarm vibration will be scaled with SCALE_NONE.
         assertEquals(1f,
                 ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(2)).getScale(), 1e-5);
-
-        // Ring vibrations have intensity OFF and are not played.
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index fd1536c..4550b56 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -1622,7 +1622,9 @@
                     ZenModeConfig.toScheduleConditionId(si),
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-            String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test");
+            // We need the package name to be something that's not "android" so there aren't any
+            // existing rules under that package.
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
             assertNotNull(id);
         }
         try {
@@ -1632,12 +1634,41 @@
                     ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
-            String id = mZenModeHelperSpy.addAutomaticZenRule("android", zenRule, "test");
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
             fail("allowed too many rules to be created");
         } catch (IllegalArgumentException e) {
             // yay
         }
+    }
 
+    @Test
+    public void testAddAutomaticZenRule_beyondSystemLimit_differentComponents() {
+        // Make sure the system limit is enforced per-package even with different component provider
+        // names.
+        for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) {
+            ScheduleInfo si = new ScheduleInfo();
+            si.startHour = i;
+            AutomaticZenRule zenRule = new AutomaticZenRule("name" + i,
+                    null,
+                    new ComponentName("android", "ScheduleConditionProvider" + i),
+                    ZenModeConfig.toScheduleConditionId(si),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            assertNotNull(id);
+        }
+        try {
+            AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                    null,
+                    new ComponentName("android", "ScheduleConditionProviderFinal"),
+                    ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            fail("allowed too many rules to be created");
+        } catch (IllegalArgumentException e) {
+            // yay
+        }
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 2477f6c..0c3b270 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -29,6 +29,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
 import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
+import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
+import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
 import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_ALWAYS;
 import static android.content.pm.ActivityInfo.LOCK_TASK_LAUNCH_MODE_DEFAULT;
@@ -182,6 +184,10 @@
 
     private final String mPackageName = getInstrumentation().getTargetContext().getPackageName();
 
+    private static final int ORIENTATION_CONFIG_CHANGES =
+            CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT | CONFIG_SCREEN_SIZE
+                    | CONFIG_SMALLEST_SCREEN_SIZE;
+
     @Before
     public void setUp() throws Exception {
         setBooted(mAtm);
@@ -487,7 +493,7 @@
     public void testSetRequestedOrientationUpdatesConfiguration() throws Exception {
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
-                .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
+                .setConfigChanges(ORIENTATION_CONFIG_CHANGES)
                 .build();
         activity.setState(RESUMED, "Testing");
 
@@ -710,7 +716,7 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
                 .setLaunchTaskBehind(true)
-                .setConfigChanges(CONFIG_ORIENTATION | CONFIG_SCREEN_LAYOUT)
+                .setConfigChanges(ORIENTATION_CONFIG_CHANGES)
                 .build();
         final Task task = activity.getTask();
         activity.setState(STOPPED, "Testing");
@@ -779,7 +785,7 @@
                     }
 
                     @Override
-                    public void onAnimationCancelled() {
+                    public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     }
                 }, 0, 0));
         activity.updateOptionsLocked(opts);
@@ -1996,7 +2002,8 @@
                     any() /* window */,  any() /* attrs */,
                     anyInt() /* viewVisibility */, anyInt() /* displayId */,
                     any() /* requestedVisibilities */, any() /* outInputChannel */,
-                    any() /* outInsetsState */, any() /* outActiveControls */);
+                    any() /* outInsetsState */, any() /* outActiveControls */,
+                    any() /* outAttachedFrame */);
             mAtm.mWindowManager.mStartingSurfaceController
                     .createTaskSnapshotSurface(activity, snapshot);
         } catch (RemoteException ignored) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
index 71f1914..b5764f5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
@@ -87,7 +87,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
         }
 
         @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 8656a4f..f2d6273 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -806,7 +806,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() throws RemoteException {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) throws RemoteException {
             mFinishedCallback = null;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 436cf36..7415460 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -522,7 +522,7 @@
         }
 
         @Override
-        public void onAnimationCancelled() {
+        public void onAnimationCancelled(boolean isKeyguardOccluded) {
             mCancelled = true;
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index d737963..40e266c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -1699,6 +1699,13 @@
         assertFalse(displayContent.mPinnedTaskController.isFreezingTaskConfig(pinnedTask));
         assertEquals(pinnedActivity.getConfiguration().orientation,
                 displayContent.getConfiguration().orientation);
+
+        // No need to apply rotation if the display ignores orientation request.
+        doCallRealMethod().when(displayContent).rotationForActivityInDifferentOrientation(any());
+        pinnedActivity.mOrientation = SCREEN_ORIENTATION_LANDSCAPE;
+        displayContent.setIgnoreOrientationRequest(true);
+        assertEquals(WindowConfiguration.ROTATION_UNDEFINED,
+                displayContent.rotationForActivityInDifferentOrientation(pinnedActivity));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index db3a51c..2956c14 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -39,7 +39,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -211,10 +210,8 @@
         assertThat(activityConfigBounds.width()).isEqualTo(activityBounds.width());
         assertThat(activityConfigBounds.height()).isEqualTo(activityBounds.height());
         assertThat(activitySizeCompatBounds.height()).isEqualTo(newTaskBounds.height());
-        final float defaultAspectRatio = mFirstActivity.mWmService.mLetterboxConfiguration
-                .getDefaultMinAspectRatioForUnresizableApps();
-        assertEquals(activitySizeCompatBounds.width(),
-                newTaskBounds.height() / defaultAspectRatio, 0.5);
+        assertThat(activitySizeCompatBounds.width()).isEqualTo(
+                newTaskBounds.height() * newTaskBounds.height() / newTaskBounds.width());
     }
 
     @Test
@@ -234,9 +231,8 @@
         assertThat(mFirstActivity.inSizeCompatMode()).isFalse();
         assertThat(taskBounds).isEqualTo(dagBounds);
         assertThat(activityBounds.width()).isEqualTo(dagBounds.width());
-        final float defaultAspectRatio = mFirstActivity.mWmService.mLetterboxConfiguration
-                .getDefaultMinAspectRatioForUnresizableApps();
-        assertEquals(activityBounds.height(), dagBounds.width() / defaultAspectRatio, 0.5);
+        assertThat(activityBounds.height())
+                .isEqualTo(dagBounds.width() * dagBounds.width() / dagBounds.height());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
index 204c7e6..027f521 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -43,6 +43,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -210,7 +211,7 @@
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
 
         adapter.onAnimationCancelled(mMockLeash);
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -226,7 +227,7 @@
         mClock.fastForward(10500);
         mHandler.timeAdvance();
 
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
         verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                 eq(adapter));
     }
@@ -247,12 +248,12 @@
             mClock.fastForward(10500);
             mHandler.timeAdvance();
 
-            verify(mMockRunner, never()).onAnimationCancelled();
+            verify(mMockRunner, never()).onAnimationCancelled(anyBoolean());
 
             mClock.fastForward(52500);
             mHandler.timeAdvance();
 
-            verify(mMockRunner).onAnimationCancelled();
+            verify(mMockRunner).onAnimationCancelled(anyBoolean());
             verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                     eq(adapter));
         } finally {
@@ -264,7 +265,7 @@
     public void testZeroAnimations() throws Exception {
         mController.goodToGo(TRANSIT_OLD_NONE);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -274,7 +275,7 @@
                 new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false);
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
     }
 
     @Test
@@ -316,7 +317,7 @@
         win.mActivityRecord.removeImmediately();
         mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN);
         verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any());
-        verify(mMockRunner).onAnimationCancelled();
+        verify(mMockRunner).onAnimationCancelled(anyBoolean());
         verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION),
                 eq(adapter));
     }
@@ -574,7 +575,7 @@
 
             // Cancel the wallpaper window animator and ensure the runner is not canceled
             wallpaperWindowToken.cancelAnimation();
-            verify(mMockRunner, never()).onAnimationCancelled();
+            verify(mMockRunner, never()).onAnimationCancelled(anyBoolean());
         } finally {
             mDisplayContent.mOpeningApps.clear();
         }
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 7f70882..324e244 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1471,6 +1471,8 @@
         final float fixedOrientationLetterboxAspectRatio = 1.1f;
         mActivity.mWmService.mLetterboxConfiguration.setFixedOrientationLetterboxAspectRatio(
                 fixedOrientationLetterboxAspectRatio);
+        mActivity.mWmService.mLetterboxConfiguration.setDefaultMinAspectRatioForUnresizableApps(
+                1.5f);
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
 
         final Rect displayBounds = new Rect(mActivity.mDisplayContent.getBounds());
@@ -1496,7 +1498,9 @@
     @Test
     public void testSplitAspectRatioForUnresizablePortraitApps() {
         // Set up a display in landscape and ignoring orientation request.
-        setUpDisplaySizeWithApp(1600, 1400);
+        int screenWidth = 1600;
+        int screenHeight = 1400;
+        setUpDisplaySizeWithApp(screenWidth, screenHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mActivity.mWmService.mLetterboxConfiguration
                         .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true);
@@ -1520,6 +1524,7 @@
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
         // Move activity to split screen which takes half of the screen.
         mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, getExpectedSplitSize(screenWidth), screenHeight);
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
         // Checking that there is no size compat mode.
@@ -1528,8 +1533,10 @@
 
     @Test
     public void testSplitAspectRatioForUnresizableLandscapeApps() {
-        // Set up a display in landscape and ignoring orientation request.
-        setUpDisplaySizeWithApp(1400, 1600);
+        // Set up a display in portrait and ignoring orientation request.
+        int screenWidth = 1400;
+        int screenHeight = 1600;
+        setUpDisplaySizeWithApp(screenWidth, screenHeight);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
         mActivity.mWmService.mLetterboxConfiguration
                         .setIsSplitScreenAspectRatioForUnresizableAppsEnabled(true);
@@ -1553,6 +1560,7 @@
                 new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
         // Move activity to split screen which takes half of the screen.
         mTask.reparent(organizer.mPrimary, POSITION_TOP, /* moveParents= */ false , "test");
+        organizer.mPrimary.setBounds(0, 0, screenWidth, getExpectedSplitSize(screenHeight));
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mTask.getWindowingMode());
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, mActivity.getWindowingMode());
         // Checking that there is no size compat mode.
@@ -2071,12 +2079,7 @@
         // Activity bounds fill split screen.
         final Rect primarySplitBounds = new Rect(organizer.mPrimary.getBounds());
         final Rect letterboxedBounds = new Rect(mActivity.getBounds());
-        // Activity is letterboxed for aspect ratio.
-        assertEquals(primarySplitBounds.height(), letterboxedBounds.height());
-        final float defaultAspectRatio = mActivity.mWmService.mLetterboxConfiguration
-                .getDefaultMinAspectRatioForUnresizableApps();
-        assertEquals(primarySplitBounds.height() / defaultAspectRatio,
-                letterboxedBounds.width(), 0.5);
+        assertEquals(primarySplitBounds, letterboxedBounds);
     }
 
     @Test
@@ -2618,6 +2621,16 @@
         assertEquals(newDensity, mActivity.getConfiguration().densityDpi);
     }
 
+    private int getExpectedSplitSize(int dimensionToSplit) {
+        int dividerWindowWidth =
+                mActivity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.docked_stack_divider_thickness);
+        int dividerInsets =
+                mActivity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.docked_stack_divider_insets);
+        return (dimensionToSplit - (dividerWindowWidth - dividerInsets * 2)) / 2;
+    }
+
     private void assertHorizontalPositionForDifferentDisplayConfigsForLandscapeActivity(
             float letterboxHorizontalPositionMultiplier) {
         // Set up a display in landscape and ignoring orientation request.
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 2a9fcb9..7f09606 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -762,12 +762,20 @@
         Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
                 ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
                 0 /* launchFlags */, candidateTask);
-        assertSame(rootTask, actualRootTask.getRootTask());
+        assertSame(rootTask, actualRootTask);
 
         // Verify the launch root task without candidate task
         actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
                 ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
                 0 /* launchFlags */);
-        assertSame(adjacentRootTask, actualRootTask.getRootTask());
+        assertSame(adjacentRootTask, actualRootTask);
+
+        final Task pinnedTask = createTask(
+                mDisplayContent, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
+        // Verify not adjusting launch target for pinned candidate task
+        actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
+                ACTIVITY_TYPE_STANDARD, null /* options */, adjacentRootTask /* sourceTask */,
+                0 /* launchFlags */, pinnedTask /* candidateTask */);
+        assertNull(actualRootTask);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 228cb65..5f30963 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -475,5 +475,13 @@
         assertFalse(activity0.isLetterboxedForFixedOrientationAndAspectRatio());
         assertFalse(activity1.isLetterboxedForFixedOrientationAndAspectRatio());
         assertEquals(SCREEN_ORIENTATION_UNSET, task.getOrientation());
+
+        tf0.setResumedActivity(activity0, "test");
+        tf1.setResumedActivity(activity1, "test");
+        mDisplayContent.mFocusedApp = activity1;
+
+        // Making the activity0 be the focused activity and ensure the focused app is updated.
+        activity0.moveFocusableActivityToTop("test");
+        assertEquals(activity0, mDisplayContent.mFocusedApp);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 5743922..1715a29 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -979,7 +979,7 @@
                     }
 
                     @Override
-                    public void onAnimationCancelled() {
+                    public void onAnimationCancelled(boolean isKeyguardOccluded) {
                     }
                 }, 0, 0, false);
         adapter.setCallingPidUid(123, 456);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
index ea18e58..739e783 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
@@ -72,7 +72,7 @@
     private static final Insets WATERFALL_INSETS = Insets.of(6, 0, 12, 0);
 
     private final WindowLayout mWindowLayout = new WindowLayout();
-    private final ClientWindowFrames mOutFrames = new ClientWindowFrames();
+    private final ClientWindowFrames mFrames = new ClientWindowFrames();
 
     private WindowManager.LayoutParams mAttrs;
     private InsetsState mState;
@@ -82,7 +82,6 @@
     private int mRequestedWidth;
     private int mRequestedHeight;
     private InsetsVisibilities mRequestedVisibilities;
-    private Rect mAttachedWindowFrame;
     private float mCompatScale;
 
     @Before
@@ -100,14 +99,14 @@
         mRequestedWidth = DISPLAY_WIDTH;
         mRequestedHeight = DISPLAY_HEIGHT;
         mRequestedVisibilities = new InsetsVisibilities();
-        mAttachedWindowFrame = null;
         mCompatScale = 1f;
+        mFrames.attachedFrame = null;
     }
 
     private void computeFrames() {
         mWindowLayout.computeFrames(mAttrs, mState, mDisplayCutoutSafe, mWindowBounds,
                 mWindowingMode, mRequestedWidth, mRequestedHeight, mRequestedVisibilities,
-                mAttachedWindowFrame, mCompatScale, mOutFrames);
+                mCompatScale, mFrames);
     }
 
     private void addDisplayCutout() {
@@ -145,9 +144,9 @@
     public void defaultParams() {
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -156,9 +155,9 @@
         mRequestedHeight = UNSPECIFIED_LENGTH;
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -172,9 +171,9 @@
         mAttrs.gravity = Gravity.LEFT | Gravity.TOP;
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertRect(0, STATUS_BAR_HEIGHT, width, STATUS_BAR_HEIGHT + height, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertRect(0, STATUS_BAR_HEIGHT, width, STATUS_BAR_HEIGHT + height, mFrames.frame);
     }
 
     @Test
@@ -186,11 +185,24 @@
         computeFrames();
 
         assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT,
-                mOutFrames.displayFrame);
+                mFrames.displayFrame);
         assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT,
-                mOutFrames.parentFrame);
+                mFrames.parentFrame);
         assertRect(0, top, DISPLAY_WIDTH, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT,
-                mOutFrames.frame);
+                mFrames.frame);
+    }
+
+    @Test
+    public void attachedFrame() {
+        final int bottom = (DISPLAY_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT) / 2;
+        mFrames.attachedFrame = new Rect(0, STATUS_BAR_HEIGHT, DISPLAY_WIDTH, bottom);
+        mRequestedWidth = UNSPECIFIED_LENGTH;
+        mRequestedHeight = UNSPECIFIED_LENGTH;
+        computeFrames();
+
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertEquals(mFrames.attachedFrame, mFrames.parentFrame);
+        assertEquals(mFrames.attachedFrame, mFrames.frame);
     }
 
     @Test
@@ -198,9 +210,9 @@
         mAttrs.setFitInsetsTypes(WindowInsets.Type.statusBars());
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.frame);
     }
 
     @Test
@@ -208,9 +220,9 @@
         mAttrs.setFitInsetsTypes(WindowInsets.Type.navigationBars());
         computeFrames();
 
-        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(0, NAVIGATION_BAR_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -218,9 +230,9 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetByTopBottom(0, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.frame);
+        assertInsetByTopBottom(0, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(0, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(0, 0, mFrames.frame);
     }
 
     @Test
@@ -228,9 +240,9 @@
         mAttrs.setFitInsetsSides(WindowInsets.Side.all());
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -238,9 +250,9 @@
         mAttrs.setFitInsetsSides(WindowInsets.Side.TOP);
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, 0, mFrames.frame);
     }
 
     @Test
@@ -248,9 +260,9 @@
         mAttrs.setFitInsetsSides(0);
         computeFrames();
 
-        assertInsetByTopBottom(0, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.frame);
+        assertInsetByTopBottom(0, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(0, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(0, 0, mFrames.frame);
     }
 
     @Test
@@ -259,9 +271,9 @@
         mState.getSource(ITYPE_NAVIGATION_BAR).setVisible(false);
         computeFrames();
 
-        assertInsetByTopBottom(0, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.frame);
+        assertInsetByTopBottom(0, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(0, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(0, 0, mFrames.frame);
     }
 
     @Test
@@ -271,9 +283,9 @@
         mAttrs.setFitInsetsIgnoringVisibility(true);
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -284,9 +296,9 @@
         mAttrs.privateFlags |= PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
         computeFrames();
 
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mOutFrames.displayFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mOutFrames.parentFrame);
-        assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mOutFrames.frame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, NAVIGATION_BAR_HEIGHT, mFrames.displayFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mFrames.parentFrame);
+        assertInsetByTopBottom(STATUS_BAR_HEIGHT, IME_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -297,11 +309,11 @@
         computeFrames();
 
         assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.displayFrame);
+                mFrames.displayFrame);
         assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.parentFrame);
+                mFrames.parentFrame);
         assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.frame);
+                mFrames.frame);
     }
 
     @Test
@@ -312,11 +324,11 @@
         computeFrames();
 
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.displayFrame);
+                mFrames.displayFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.parentFrame);
+                mFrames.parentFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.frame);
+                mFrames.frame);
     }
 
     @Test
@@ -327,9 +339,9 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.displayFrame);
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.parentFrame);
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.frame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.displayFrame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.parentFrame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.frame);
     }
 
     @Test
@@ -344,9 +356,9 @@
         mAttrs.privateFlags |= PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
         computeFrames();
 
-        assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mOutFrames.displayFrame);
-        assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mOutFrames.parentFrame);
-        assertRect(0, 0, DISPLAY_WIDTH, height + DISPLAY_CUTOUT_HEIGHT, mOutFrames.frame);
+        assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mFrames.displayFrame);
+        assertRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, mFrames.parentFrame);
+        assertRect(0, 0, DISPLAY_WIDTH, height + DISPLAY_CUTOUT_HEIGHT, mFrames.frame);
     }
 
     @Test
@@ -359,11 +371,11 @@
         computeFrames();
 
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.displayFrame);
+                mFrames.displayFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.parentFrame);
+                mFrames.parentFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.frame);
+                mFrames.frame);
     }
 
     @Test
@@ -373,9 +385,9 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.displayFrame);
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.parentFrame);
-        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mOutFrames.frame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.displayFrame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.parentFrame);
+        assertInsetBy(WATERFALL_INSETS.left, 0, WATERFALL_INSETS.right, 0, mFrames.frame);
     }
 
     @Test
@@ -386,11 +398,11 @@
         computeFrames();
 
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.displayFrame);
+                mFrames.displayFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.parentFrame);
+                mFrames.parentFrame);
         assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
-                mOutFrames.frame);
+                mFrames.frame);
     }
 
     @Test
@@ -400,8 +412,8 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetByTopBottom(0, 0, mOutFrames.displayFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.parentFrame);
-        assertInsetByTopBottom(0, 0, mOutFrames.frame);
+        assertInsetByTopBottom(0, 0, mFrames.displayFrame);
+        assertInsetByTopBottom(0, 0, mFrames.parentFrame);
+        assertInsetByTopBottom(0, 0, mFrames.frame);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index a0c20c2..1a64f5e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -49,6 +49,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.pm.PackageManager;
+import android.graphics.Rect;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -60,6 +61,7 @@
 import android.view.InsetsVisibilities;
 import android.view.View;
 import android.view.WindowManager;
+import android.window.WindowContainerToken;
 
 import androidx.test.filters.SmallTest;
 
@@ -281,7 +283,7 @@
 
         mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY,
                 UserHandle.USER_SYSTEM, new InsetsVisibilities(), null, new InsetsState(),
-                new InsetsSourceControl[0]);
+                new InsetsSourceControl[0], new Rect());
 
         verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(),
                 any(), anyInt(), anyInt(), any());
@@ -316,4 +318,76 @@
         verify(mWm.mInputManager).setInTouchMode(
                 !currentTouchMode, callingPid, callingUid, /* hasPermission= */ false);
     }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() {
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(null);
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() {
+        Binder cookie = new Binder("test cookie");
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isNull();
+
+        final ActivityRecord testActivity = new ActivityBuilder(mAtm)
+                .setCreateTask(true)
+                .build();
+
+        wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() {
+        final Binder cookie = new Binder("ginger cookie");
+        final WindowContainerToken launchRootTask = mock(WindowContainerToken.class);
+        setupActivityWithLaunchCookie(cookie, launchRootTask);
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie);
+        assertThat(wct).isEqualTo(launchRootTask);
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() {
+        final Binder cookie1 = new Binder("ginger cookie");
+        final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class);
+        setupActivityWithLaunchCookie(cookie1, launchRootTask1);
+
+        setupActivityWithLaunchCookie(new Binder("choc chip cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
+                mock(WindowContainerToken.class));
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie1);
+        assertThat(wct).isEqualTo(launchRootTask1);
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() {
+        setupActivityWithLaunchCookie(new Binder("ginger cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("choc chip cookie"),
+                mock(WindowContainerToken.class));
+
+        setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
+                mock(WindowContainerToken.class));
+
+        WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(
+                new Binder("some other cookie"));
+        assertThat(wct).isNull();
+    }
+
+    private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) {
+        final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class);
+        when(remoteToken.toWindowContainerToken()).thenReturn(wct);
+        final ActivityRecord testActivity = new ActivityBuilder(mAtm)
+                .setCreateTask(true)
+                .build();
+        testActivity.mLaunchCookie = launchCookie;
+        testActivity.getTask().mRemoteToken = remoteToken;
+    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index cc33f88..26a1e9d 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -16,6 +16,7 @@
 
 package com.android.server.usage;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.usage.TimeSparseArray;
 import android.app.usage.UsageEvents;
@@ -24,6 +25,7 @@
 import android.os.Build;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -55,8 +57,11 @@
 import java.nio.file.StandardCopyOption;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Provides an interface to query for UsageStat data from a Protocol Buffer database.
@@ -1252,6 +1257,10 @@
             Slog.wtf(TAG, "Attempting to backup UsageStats as XML with version " + version);
             return null;
         }
+        if (version < 1 || version > BACKUP_VERSION) {
+            Slog.wtf(TAG, "Attempting to backup UsageStats with an unknown version: " + version);
+            return null;
+        }
         synchronized (mLock) {
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             if (KEY_USAGE_STATS.equals(key)) {
@@ -1300,14 +1309,26 @@
             }
             return baos.toByteArray();
         }
+    }
 
+    /**
+     * Updates the set of packages given to only include those that have been used within the
+     * given timeframe (as defined by {@link UsageStats#getLastTimePackageUsed()}).
+     */
+    private void calculatePackagesUsedWithinTimeframe(
+            IntervalStats stats, Set<String> packagesList, long timeframeMs) {
+        for (UsageStats stat : stats.packageStats.values()) {
+            if (stat.getLastTimePackageUsed() > timeframeMs) {
+                packagesList.add(stat.mPackageName);
+            }
+        }
     }
 
     /**
      * @hide
      */
     @VisibleForTesting
-    public void applyRestoredPayload(String key, byte[] payload) {
+    public @NonNull Set<String> applyRestoredPayload(String key, byte[] payload) {
         synchronized (mLock) {
             if (KEY_USAGE_STATS.equals(key)) {
                 // Read stats files for the current device configs
@@ -1320,12 +1341,15 @@
                 IntervalStats yearlyConfigSource =
                         getLatestUsageStats(UsageStatsManager.INTERVAL_YEARLY);
 
+                final Set<String> packagesRestored = new ArraySet<>();
                 try {
                     DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
                     int backupDataVersion = in.readInt();
 
                     // Can't handle this backup set
-                    if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) return;
+                    if (backupDataVersion < 1 || backupDataVersion > BACKUP_VERSION) {
+                        return packagesRestored;
+                    }
 
                     // Delete all stats files
                     // Do this after reading version and before actually restoring
@@ -1333,10 +1357,14 @@
                         deleteDirectoryContents(mIntervalDirs[i]);
                     }
 
+                    // 90 days before today in epoch
+                    final long timeframe = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(90);
                     int fileCount = in.readInt();
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
+                        packagesRestored.addAll(stats.packageStats.keySet());
                         stats = mergeStats(stats, dailyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_DAILY, stats);
                     }
@@ -1345,6 +1373,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, weeklyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_WEEKLY, stats);
                     }
@@ -1353,6 +1382,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, monthlyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_MONTHLY, stats);
                     }
@@ -1361,6 +1391,7 @@
                     for (int i = 0; i < fileCount; i++) {
                         IntervalStats stats = deserializeIntervalStats(getIntervalStatsBytes(in),
                                 backupDataVersion);
+                        calculatePackagesUsedWithinTimeframe(stats, packagesRestored, timeframe);
                         stats = mergeStats(stats, yearlyConfigSource);
                         putUsageStats(UsageStatsManager.INTERVAL_YEARLY, stats);
                     }
@@ -1370,7 +1401,9 @@
                 } finally {
                     indexFilesLocked();
                 }
+                return packagesRestored;
             }
+            return Collections.EMPTY_SET;
         }
     }
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index ef13cd9..f595c3d 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -3034,7 +3034,8 @@
                     if (userStats == null) {
                         return; // user was stopped or removed
                     }
-                    userStats.applyRestoredPayload(key, payload);
+                    final Set<String> restoredApps = userStats.applyRestoredPayload(key, payload);
+                    mAppStandby.restoreAppsToRare(restoredApps, user);
                 }
             }
         }
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index c609add..34c6c16 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -63,6 +63,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A per-user UsageStatsService. All methods are meant to be called with the main lock held
@@ -1374,8 +1375,8 @@
         return mDatabase.getBackupPayload(key);
     }
 
-    void applyRestoredPayload(String key, byte[] payload){
+    Set<String> applyRestoredPayload(String key, byte[] payload) {
         checkAndGetTimeLocked();
-        mDatabase.applyRestoredPayload(key, payload);
+        return mDatabase.applyRestoredPayload(key, payload);
     }
 }
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 434663b..25db81f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -31,11 +31,18 @@
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED__RESULT__CALLBACK_INIT_STATE_UNKNOWN_TIMEOUT;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__APP_REQUEST_UPDATE_STATE;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__CALLBACK_UPDATE_STATE_AFTER_TIMEOUT;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECTED;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__ON_DISCONNECTED;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_BIND_SERVICE_FAIL;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__REQUEST_UPDATE_STATE;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__START_EXTERNAL_SOURCE_DETECTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__START_SOFTWARE_DETECTION;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP;
 import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED;
@@ -146,6 +153,13 @@
     private static final int METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK =
             HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECT_UNEXPECTED_CALLBACK;
 
+    private static final int METRICS_EXTERNAL_SOURCE_DETECTED =
+            HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECTED;
+    private static final int METRICS_EXTERNAL_SOURCE_REJECTED =
+            HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_REJECTED;
+    private static final int METRICS_EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION =
+            HOTWORD_DETECTOR_EVENTS__EVENT__EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION;
+
     private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool();
     // TODO: This may need to be a Handler(looper)
     private final ScheduledExecutorService mScheduledExecutorService =
@@ -382,6 +396,10 @@
     }
 
     void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) {
+        HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                HOTWORD_DETECTOR_EVENTS__EVENT__APP_REQUEST_UPDATE_STATE,
+                mVoiceInteractionServiceUid);
+
         // Prevent doing the init late, so restart is handled equally to a clean process start.
         // TODO(b/191742511): this logic needs a test
         if (!mUpdateStateAfterStartFinished.get()
@@ -422,14 +440,23 @@
                     Slog.d(TAG, "onDetected");
                 }
                 synchronized (mLock) {
+                    HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+                            mDetectorType,
+                            HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
                     if (!mPerformingSoftwareHotwordDetection) {
                         Slog.i(TAG, "Hotword detection has already completed");
+                        HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+                                mDetectorType,
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK);
                         return;
                     }
                     mPerformingSoftwareHotwordDetection = false;
                     try {
                         enforcePermissionsForDataDelivery();
                     } catch (SecurityException e) {
+                        HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+                                mDetectorType,
+                                METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
                         mSoftwareCallback.onError();
                         return;
                     }
@@ -449,6 +476,9 @@
                 if (DEBUG) {
                     Slog.wtf(TAG, "onRejected");
                 }
+                HotwordMetricsLogger.writeKeyphraseTriggerEvent(
+                        mDetectorType,
+                        HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
                 // onRejected isn't allowed here, and we are not expecting it.
             }
         };
@@ -460,6 +490,9 @@
                         null,
                         null,
                         internalCallback));
+        HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                HOTWORD_DETECTOR_EVENTS__EVENT__START_SOFTWARE_DETECTION,
+                mVoiceInteractionServiceUid);
     }
 
     public void startListeningFromExternalSource(
@@ -891,6 +924,9 @@
                                 @Override
                                 public void onRejected(HotwordRejectedResult result)
                                         throws RemoteException {
+                                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                                            METRICS_EXTERNAL_SOURCE_REJECTED,
+                                            mVoiceInteractionServiceUid);
                                     mScheduledExecutorService.schedule(
                                             () -> {
                                                 bestEffortClose(serviceAudioSink, audioSource);
@@ -912,6 +948,9 @@
                                 @Override
                                 public void onDetected(HotwordDetectedResult triggerResult)
                                         throws RemoteException {
+                                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                                            METRICS_EXTERNAL_SOURCE_DETECTED,
+                                            mVoiceInteractionServiceUid);
                                     mScheduledExecutorService.schedule(
                                             () -> {
                                                 bestEffortClose(serviceAudioSink, audioSource);
@@ -922,6 +961,9 @@
                                     try {
                                         enforcePermissionsForDataDelivery();
                                     } catch (SecurityException e) {
+                                        HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                                                METRICS_EXTERNAL_SOURCE_DETECT_SECURITY_EXCEPTION,
+                                                mVoiceInteractionServiceUid);
                                         callback.onError();
                                         return;
                                     }
@@ -942,6 +984,9 @@
                     // A copy of this has been created and passed to the hotword validator
                     bestEffortClose(serviceAudioSource);
                 });
+        HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                HOTWORD_DETECTOR_EVENTS__EVENT__START_EXTERNAL_SOURCE_DETECTION,
+                mVoiceInteractionServiceUid);
     }
 
     private class ServiceConnectionFactory {
@@ -1002,7 +1047,12 @@
                     return;
                 }
                 mIsBound = connected;
-                if (connected && !mIsLoggedFirstConnect) {
+
+                if (!connected) {
+                    HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+                            HOTWORD_DETECTOR_EVENTS__EVENT__ON_DISCONNECTED,
+                            mVoiceInteractionServiceUid);
+                } else if (!mIsLoggedFirstConnect) {
                     mIsLoggedFirstConnect = true;
                     HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
                             HOTWORD_DETECTOR_EVENTS__EVENT__ON_CONNECTED,
diff --git a/telecomm/java/android/telecom/InCallService.java b/telecomm/java/android/telecom/InCallService.java
index 0ddd52d..64a86db 100644
--- a/telecomm/java/android/telecom/InCallService.java
+++ b/telecomm/java/android/telecom/InCallService.java
@@ -22,6 +22,7 @@
 import android.app.Service;
 import android.app.UiModeManager;
 import android.bluetooth.BluetoothDevice;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.hardware.camera2.CameraManager;
 import android.net.Uri;
@@ -47,7 +48,7 @@
  * in a call.  It also provides the user with a means to initiate calls and see a history of calls
  * on their device.  A device is bundled with a system provided default dialer/phone app.  The user
  * may choose a single app to take over this role from the system app.  An app which wishes to
- * fulfill one this role uses the {@link android.app.role.RoleManager} to request that they fill the
+ * fulfill this role uses the {@link android.app.role.RoleManager} to request that they fill the
  * {@link android.app.role.RoleManager#ROLE_DIALER} role.
  * <p>
  * The default phone app provides a user interface while the device is in a call, and the device is
@@ -63,13 +64,23 @@
  *     UI, as well as an ongoing call UI.</li>
  * </ul>
  * <p>
- * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} crashes during
- * {@link InCallService} binding, the Telecom framework will automatically fall back to using the
- * dialer app pre-loaded on the device.  The system will display a notification to the user to let
- * them know that the app has crashed and that their call was continued using the pre-loaded dialer
- * app.
+ * Note: If the app filling the {@link android.app.role.RoleManager#ROLE_DIALER} returns a
+ * {@code null} {@link InCallService} during binding, the Telecom framework will automatically fall
+ * back to using the dialer app preloaded on the device.  The system will display a notification to
+ * the user to let them know that their call was continued using the preloaded dialer app.  Your
+ * app should never return a {@code null} binding; doing so means it does not fulfil the
+ * requirements of {@link android.app.role.RoleManager#ROLE_DIALER}.
  * <p>
- * The pre-loaded dialer will ALWAYS be used when the user places an emergency call, even if your
+ * Note: If your app fills {@link android.app.role.RoleManager#ROLE_DIALER} and makes changes at
+ * runtime which cause it to no longer fulfil the requirements of this role,
+ * {@link android.app.role.RoleManager} will automatically remove your app from the role and close
+ * your app.  For example, if you use
+ * {@link android.content.pm.PackageManager#setComponentEnabledSetting(ComponentName, int, int)} to
+ * programmatically disable the {@link InCallService} your app declares in its manifest, your app
+ * will no longer fulfil the requirements expected of
+ * {@link android.app.role.RoleManager#ROLE_DIALER}.
+ * <p>
+ * The preloaded dialer will ALWAYS be used when the user places an emergency call, even if your
  * app fills the {@link android.app.role.RoleManager#ROLE_DIALER} role.  To ensure an optimal
  * experience when placing an emergency call, the default dialer should ALWAYS use
  * {@link android.telecom.TelecomManager#placeCall(Uri, Bundle)} to place calls (including
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index 0e5a177..315c40f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -21,8 +21,10 @@
 import com.android.server.wm.flicker.traces.region.RegionSubject
 import com.android.server.wm.traces.common.FlickerComponentName
 
-val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher",
-        "com.google.android.apps.nexuslauncher.NexusLauncherActivity")
+val LAUNCHER_COMPONENT = FlickerComponentName(
+    "com.google.android.apps.nexuslauncher",
+    "com.google.android.apps.nexuslauncher.NexusLauncherActivity"
+)
 
 /**
  * Checks that [FlickerComponentName.STATUS_BAR] window is visible and above the app windows in
@@ -110,9 +112,9 @@
 fun FlickerTestParameter.navBarLayerPositionStart() {
     assertLayersStart {
         val display = this.entry.displays.minByOrNull { it.id }
-                ?: throw RuntimeException("There is no display!")
+            ?: throw RuntimeException("There is no display!")
         this.visibleRegion(FlickerComponentName.NAV_BAR)
-                .coversExactly(WindowUtils.getNavigationBarPosition(display))
+            .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation))
     }
 }
 
@@ -123,9 +125,9 @@
 fun FlickerTestParameter.navBarLayerPositionEnd() {
     assertLayersEnd {
         val display = this.entry.displays.minByOrNull { it.id }
-                ?: throw RuntimeException("There is no display!")
+            ?: throw RuntimeException("There is no display!")
         this.visibleRegion(FlickerComponentName.NAV_BAR)
-                .coversExactly(WindowUtils.getNavigationBarPosition(display))
+            .coversExactly(WindowUtils.getNavigationBarPosition(display, isGesturalNavigation))
     }
 }
 
@@ -244,11 +246,11 @@
 
     assertLayersStart {
         this.isVisible(originalLayer)
-                .isInvisible(newLayer)
+            .isInvisible(newLayer)
     }
 
     assertLayersEnd {
         this.isInvisible(originalLayer)
-                .isVisible(newLayer)
+            .isVisible(newLayer)
     }
 }