Merge "Fix NPE for CompanionDeviceActivity.setResultAndFinish()"
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/RecentsAnimationPerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/RecentsAnimationPerfTest.java
index 98b5938..1c40a06 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/RecentsAnimationPerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/RecentsAnimationPerfTest.java
@@ -25,7 +25,6 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
-import android.window.TaskSnapshot;
 import android.app.IActivityTaskManager;
 import android.content.ComponentName;
 import android.content.Context;
@@ -41,6 +40,7 @@
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationTarget;
+import android.window.TaskSnapshot;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.lifecycle.Stage;
@@ -210,7 +210,8 @@
             }
 
             @Override
-            public void onAnimationCanceled(TaskSnapshot taskSnapshot) throws RemoteException {
+            public void onAnimationCanceled(int[] taskIds, TaskSnapshot[] taskSnapshots)
+                    throws RemoteException {
                 Assume.assumeNoException(
                         new AssertionError("onAnimationCanceled should not be called"));
             }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index bc34c50..258adcf 100755
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -118,6 +118,7 @@
     field public static final String HDMI_CEC = "android.permission.HDMI_CEC";
     field @Deprecated public static final String HIDE_NON_SYSTEM_OVERLAY_WINDOWS = "android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS";
     field public static final String INJECT_EVENTS = "android.permission.INJECT_EVENTS";
+    field public static final String INSTALL_DPC_PACKAGES = "android.permission.INSTALL_DPC_PACKAGES";
     field public static final String INSTALL_DYNAMIC_SYSTEM = "android.permission.INSTALL_DYNAMIC_SYSTEM";
     field public static final String INSTALL_GRANT_RUNTIME_PERMISSIONS = "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS";
     field public static final String INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE = "android.permission.INSTALL_LOCATION_TIME_ZONE_PROVIDER_SERVICE";
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 1b5bb86..40880d4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -13351,8 +13351,8 @@
     }
 
     /**
-     * Returns the default package names set by the OEM that are allowed to request user consent for
-     * cross-profile communication without being explicitly enabled by the admin, via {@link
+     * Returns the default package names set by the OEM that are allowed to communicate
+     * cross-profile without being explicitly enabled by the admin, via {@link
      * com.android.internal.R.array#cross_profile_apps} and {@link com.android.internal.R.array
      * #vendor_cross_profile_apps}.
      *
diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java
index a0d2977..8e8c035 100644
--- a/core/java/android/app/admin/DevicePolicyManagerInternal.java
+++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java
@@ -200,10 +200,10 @@
     public abstract List<String> getAllCrossProfilePackages();
 
     /**
-     * Returns the default package names set by the OEM that are allowed to request user consent for
-     * cross-profile communication without being explicitly enabled by the admin, via
-     * {@link com.android.internal.R.array#cross_profile_apps} and
-     * {@link com.android.internal.R.array#vendor_cross_profile_apps}.
+     * Returns the default package names set by the OEM that are allowed to communicate
+     * cross-profile without being explicitly enabled by the admin, via {@link
+     * com.android.internal.R.array#cross_profile_apps} and {@link
+     * com.android.internal.R.array#vendor_cross_profile_apps}.
      *
      * @hide
      */
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index a8fe602..6719a69 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -109,7 +109,7 @@
     }
 
     private final ICompanionDeviceManager mService;
-    private final Context mContext;
+    private Context mContext;
 
     /** @hide */
     public CompanionDeviceManager(
@@ -572,6 +572,7 @@
                 mCallback = null;
                 mHandler = null;
                 mRequest = null;
+                mContext = null;
             }
         }
 
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index cbb5c2b..e7ca76e 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -23,6 +23,7 @@
 import android.app.compat.CompatChanges;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.Disabled;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.Overridable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
@@ -1042,6 +1043,14 @@
     public static final float OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE = 16 / 9f;
 
     /**
+     * Compares activity window layout min width/height with require space for multi window to
+     * determine if it can be put into multi window mode.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
+    private static final long CHECK_MIN_WIDTH_HEIGHT_FOR_MULTI_WINDOW = 197654537L;
+
+    /**
      * Convert Java change bits to native.
      *
      * @hide
@@ -1481,6 +1490,17 @@
         }
     }
 
+    /**
+     * Whether we should compare activity window layout min width/height with require space for
+     * multi window to determine if it can be put into multi window mode.
+     * @hide
+     */
+    public boolean shouldCheckMinWidthHeightForMultiWindow() {
+        return CompatChanges.isChangeEnabled(CHECK_MIN_WIDTH_HEIGHT_FOR_MULTI_WINDOW,
+                applicationInfo.packageName,
+                UserHandle.getUserHandleForUid(applicationInfo.uid));
+    }
+
     public void dump(Printer pw, String prefix) {
         dump(pw, prefix, DUMP_FLAG_ALL);
     }
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index 259cbf9..5ffb958 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -128,6 +128,11 @@
      */
     private final int mRollbackDataPolicy;
 
+    /**
+     * Indicates if this app contains a {@link android.app.admin.DeviceAdminReceiver}.
+     */
+    private final boolean mHasDeviceAdminReceiver;
+
     public ApkLite(String path, String packageName, String splitName, boolean isFeatureSplit,
             String configForSplit, String usesSplitName, boolean isSplitRequired, int versionCode,
             int versionCodeMajor, int revisionCode, int installLocation,
@@ -137,7 +142,8 @@
             String targetPackageName, boolean overlayIsStatic, int overlayPriority,
             String requiredSystemPropertyName, String requiredSystemPropertyValue,
             int minSdkVersion, int targetSdkVersion, int rollbackDataPolicy,
-            Set<String> requiredSplitTypes, Set<String> splitTypes) {
+            Set<String> requiredSplitTypes, Set<String> splitTypes,
+            boolean hasDeviceAdminReceiver) {
         mPath = path;
         mPackageName = packageName;
         mSplitName = splitName;
@@ -169,6 +175,7 @@
         mMinSdkVersion = minSdkVersion;
         mTargetSdkVersion = targetSdkVersion;
         mRollbackDataPolicy = rollbackDataPolicy;
+        mHasDeviceAdminReceiver = hasDeviceAdminReceiver;
     }
 
     /**
@@ -461,11 +468,16 @@
         return mRollbackDataPolicy;
     }
 
+    @DataClass.Generated.Member
+    public boolean isHasDeviceAdminReceiver() {
+        return mHasDeviceAdminReceiver;
+    }
+
     @DataClass.Generated(
-            time = 1631763761543L,
+            time = 1635266936769L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
-            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+            inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final  int mVersionCodeMajor\nprivate final  int mVersionCode\nprivate final  int mRevisionCode\nprivate final  int mInstallLocation\nprivate final  int mMinSdkVersion\nprivate final  int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final  boolean mFeatureSplit\nprivate final  boolean mIsolatedSplits\nprivate final  boolean mSplitRequired\nprivate final  boolean mCoreApp\nprivate final  boolean mDebuggable\nprivate final  boolean mProfileableByShell\nprivate final  boolean mMultiArch\nprivate final  boolean mUse32bitAbi\nprivate final  boolean mExtractNativeLibs\nprivate final  boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final  boolean mOverlayIsStatic\nprivate final  int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final  int mRollbackDataPolicy\nprivate final  boolean mHasDeviceAdminReceiver\npublic  long getLongVersionCode()\nprivate  boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 82637aa..1639ee9 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -28,6 +28,7 @@
 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
 
 import android.annotation.NonNull;
+import android.app.admin.DeviceAdminReceiver;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.SigningDetails;
@@ -399,6 +400,8 @@
         String requiredSystemPropertyName = null;
         String requiredSystemPropertyValue = null;
 
+        boolean hasDeviceAdminReceiver = false;
+
         // Only search the tree when the tag is the direct child of <manifest> tag
         int type;
         final int searchDepth = parser.getDepth() + 1;
@@ -432,6 +435,10 @@
                         "useEmbeddedDex", false);
                 rollbackDataPolicy = parser.getAttributeIntValue(ANDROID_RES_NAMESPACE,
                         "rollbackDataPolicy", 0);
+                String permission = parser.getAttributeValue(ANDROID_RES_NAMESPACE,
+                        "permission");
+                boolean hasBindDeviceAdminPermission =
+                        android.Manifest.permission.BIND_DEVICE_ADMIN.equals(permission);
 
                 final int innerDepth = parser.getDepth();
                 int innerType;
@@ -449,6 +456,9 @@
                     if (ParsingPackageUtils.TAG_PROFILEABLE.equals(parser.getName())) {
                         profilableByShell = parser.getAttributeBooleanValue(ANDROID_RES_NAMESPACE,
                                 "shell", profilableByShell);
+                    } else if (ParsingPackageUtils.TAG_RECEIVER.equals(parser.getName())) {
+                        hasDeviceAdminReceiver |= isDeviceAdminReceiver(
+                                parser, hasBindDeviceAdminPermission);
                     }
                 }
             } else if (ParsingPackageUtils.TAG_OVERLAY.equals(parser.getName())) {
@@ -541,7 +551,42 @@
                         useEmbeddedDex, extractNativeLibs, isolatedSplits, targetPackage,
                         overlayIsStatic, overlayPriority, requiredSystemPropertyName,
                         requiredSystemPropertyValue, minSdkVersion, targetSdkVersion,
-                        rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second));
+                        rollbackDataPolicy, requiredSplitTypes.first, requiredSplitTypes.second,
+                        hasDeviceAdminReceiver));
+    }
+
+    private static boolean isDeviceAdminReceiver(
+            XmlResourceParser parser, boolean applicationHasBindDeviceAdminPermission)
+            throws XmlPullParserException, IOException {
+        String permission = parser.getAttributeValue(ANDROID_RES_NAMESPACE,
+                "permission");
+        if (!applicationHasBindDeviceAdminPermission
+                && !android.Manifest.permission.BIND_DEVICE_ADMIN.equals(permission)) {
+            return false;
+        }
+
+        boolean hasDeviceAdminReceiver = false;
+        final int depth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > depth)) {
+            if (type == XmlPullParser.END_TAG
+                    || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            if (parser.getDepth() != depth + 1) {
+                // Search only under <receiver>.
+                continue;
+            }
+            if (!hasDeviceAdminReceiver && "meta-data".equals(parser.getName())) {
+                String name = parser.getAttributeValue(ANDROID_RES_NAMESPACE,
+                        "name");
+                if (DeviceAdminReceiver.DEVICE_ADMIN_META_DATA.equals(name)) {
+                    hasDeviceAdminReceiver = true;
+                }
+            }
+        }
+        return hasDeviceAdminReceiver;
     }
 
     public static ParseResult<Pair<String, String>> parsePackageSplitNames(ParseInput input,
diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
index d19186e..0a10aaa 100644
--- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java
@@ -52,7 +52,6 @@
 import android.content.pm.parsing.component.ComponentMutateUtils;
 import android.content.pm.parsing.component.ComponentParseUtils;
 import android.content.pm.parsing.component.ParsedActivity;
-import android.content.pm.parsing.component.ParsedActivityImpl;
 import android.content.pm.parsing.component.ParsedActivityUtils;
 import android.content.pm.parsing.component.ParsedAttribution;
 import android.content.pm.parsing.component.ParsedAttributionUtils;
@@ -191,6 +190,7 @@
     public static final String TAG_USES_SDK = "uses-sdk";
     public static final String TAG_USES_SPLIT = "uses-split";
     public static final String TAG_PROFILEABLE = "profileable";
+    public static final String TAG_RECEIVER = "receiver";
 
     public static final String METADATA_MAX_ASPECT_RATIO = "android.max_aspect";
     public static final String METADATA_SUPPORTS_SIZE_CHANGES = "android.supports_size_changes";
diff --git a/core/java/android/hardware/biometrics/BiometricOverlayConstants.java b/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
index 603b06d..065ae64a 100644
--- a/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricOverlayConstants.java
@@ -38,13 +38,16 @@
     int REASON_AUTH_KEYGUARD = 4;
     /** Non-specific usage (from FingerprintManager). */
     int REASON_AUTH_OTHER = 5;
+    /** Usage from Settings. */
+    int REASON_AUTH_SETTINGS = 6;
 
     @IntDef({REASON_UNKNOWN,
             REASON_ENROLL_FIND_SENSOR,
             REASON_ENROLL_ENROLLING,
             REASON_AUTH_BP,
             REASON_AUTH_KEYGUARD,
-            REASON_AUTH_OTHER})
+            REASON_AUTH_OTHER,
+            REASON_AUTH_SETTINGS})
     @Retention(RetentionPolicy.SOURCE)
     @interface ShowReason {}
 }
diff --git a/core/java/android/net/NetworkTemplate.java b/core/java/android/net/NetworkTemplate.java
index 74506da..ee24084 100644
--- a/core/java/android/net/NetworkTemplate.java
+++ b/core/java/android/net/NetworkTemplate.java
@@ -777,8 +777,8 @@
     }
 
     /**
-     * Examine the given template and normalize if it refers to a "merged"
-     * mobile subscriber. We pick the "lowest" merged subscriber as the primary
+     * Examine the given template and normalize it.
+     * We pick the "lowest" merged subscriber as the primary
      * for key purposes, and expand the template to match all other merged
      * subscribers.
      * <p>
@@ -793,8 +793,8 @@
     }
 
     /**
-     * Examine the given template and normalize if it refers to a "merged"
-     * mobile subscriber. We pick the "lowest" merged subscriber as the primary
+     * Examine the given template and normalize it.
+     * We pick the "lowest" merged subscriber as the primary
      * for key purposes, and expand the template to match all other merged
      * subscribers.
      *
@@ -806,7 +806,12 @@
      * A, but also matches B.
      */
     public static NetworkTemplate normalize(NetworkTemplate template, List<String[]> mergedList) {
-        if (!template.isMatchRuleMobile()) return template;
+        // Now there are several types of network which uses SubscriberId to store network
+        // information. For instances:
+        // The TYPE_WIFI with subscriberId means that it is a merged carrier wifi network.
+        // The TYPE_CARRIER means that the network associate to specific carrier network.
+
+        if (template.mSubscriberId == null) return template;
 
         for (String[] merged : mergedList) {
             if (ArrayUtils.contains(merged, template.mSubscriberId)) {
diff --git a/core/java/android/view/IRecentsAnimationRunner.aidl b/core/java/android/view/IRecentsAnimationRunner.aidl
index 8111755..aca17e4 100644
--- a/core/java/android/view/IRecentsAnimationRunner.aidl
+++ b/core/java/android/view/IRecentsAnimationRunner.aidl
@@ -35,15 +35,17 @@
      * wallpaper not drawing in time, or the handler not finishing the animation within a predefined
      * amount of time.
      *
-     * @param taskSnapshot If the snapshot is null, the animation will be cancelled and the leash
-     *                     will be inactive immediately. Otherwise, the contents of the task will be
-     *                     replaced with {@param taskSnapshot}, such that the runner's leash is
-     *                     still active. As soon as the runner doesn't need the leash anymore, it
-     *                     must call {@link IRecentsAnimationController#cleanupScreenshot).
+     * @param taskIds Indicates tasks with cancelling snapshot.
+     * @param taskSnapshots If the snapshots is null, the animation will be cancelled and the leash
+     *                      will be inactive immediately. Otherwise, the contents of the tasks will
+     *                      be replaced with {@param taskSnapshots}, such that the runner's leash is
+     *                      still active. As soon as the runner doesn't need the leash anymore, it
+     *                      must call {@link IRecentsAnimationController#cleanupScreenshot).
      *
      * @see {@link RecentsAnimationController#cleanupScreenshot}
      */
-    void onAnimationCanceled(in @nullable TaskSnapshot taskSnapshot) = 1;
+    void onAnimationCanceled(in @nullable int[] taskIds,
+            in @nullable TaskSnapshot[] taskSnapshots) = 1;
 
     /**
      * Called when the system is ready for the handler to start animating all the visible tasks.
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 1dd5a1b..71c1b7c 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1266,6 +1266,11 @@
     public void notifyFinished(InsetsAnimationControlRunner runner, boolean shown) {
         cancelAnimation(runner, false /* invokeCallback */);
         if (DEBUG) Log.d(TAG, "notifyFinished. shown: " + shown);
+        if (runner.getAnimationType() == ANIMATION_TYPE_RESIZE) {
+            // The resize animation doesn't show or hide the insets. We shouldn't change the
+            // requested visibility.
+            return;
+        }
         if (shown) {
             showDirectly(runner.getTypes(), true /* fromIme */);
         } else {
diff --git a/core/java/android/view/InsetsResizeAnimationRunner.java b/core/java/android/view/InsetsResizeAnimationRunner.java
index e1352dd..edcfc95 100644
--- a/core/java/android/view/InsetsResizeAnimationRunner.java
+++ b/core/java/android/view/InsetsResizeAnimationRunner.java
@@ -131,6 +131,9 @@
 
     @Override
     public boolean applyChangeInsets(InsetsState outState) {
+        if (mCancelled) {
+            return false;
+        }
         final float fraction = mAnimation.getInterpolatedFraction();
         for (@InternalInsetsType int type = 0; type < InsetsState.SIZE; type++) {
             final InsetsSource fromSource = mFromState.peekSource(type);
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 66fcf6c..240d3aa 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4215,6 +4215,16 @@
     <permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE"
                 android:protectionLevel="signature" />
 
+    <!-- @SystemApi Allows an application to install DPCs only, an application is
+         considered a DPC if it has a {@link android.app.admin.DeviceAdminReceiver}
+         protected by {@link android.Manifest.permission#BIND_DEVICE_ADMIN).
+         This is a limited version of
+         {@link android.Manifest.permission#INSTALL_PACKAGES}.
+         @hide
+    -->
+    <permission android:name="android.permission.INSTALL_DPC_PACKAGES"
+                android:protectionLevel="signature|role" />
+
     <!-- Allows an application to use System Data Loaders.
          <p>Not for use by third-party applications.
          @hide
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index a564949..ac2e448 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -240,8 +240,12 @@
     @WMSingleton
     @Provides
     static FullscreenTaskListener provideFullscreenTaskListener(
-            SyncTransactionQueue syncQueue, Optional<FullscreenUnfoldController> controller) {
-        return new FullscreenTaskListener(syncQueue, controller);
+            SyncTransactionQueue syncQueue,
+            Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController,
+            Optional<RecentTasksController> recentTasksOptional
+    ) {
+        return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController,
+                recentTasksOptional);
     }
 
     //
@@ -490,12 +494,13 @@
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
                     rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController,
                     displayInsetsController, transitions, transactionPool, iconProvider,
-                    stageTaskUnfoldControllerProvider));
+                    recentTasks, stageTaskUnfoldControllerProvider));
         } else {
             return Optional.empty();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 3f17f2b..6e38e42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -35,6 +35,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -47,15 +48,23 @@
     private static final String TAG = "FullscreenTaskListener";
 
     private final SyncTransactionQueue mSyncQueue;
+    private final FullscreenUnfoldController mFullscreenUnfoldController;
+    private final Optional<RecentTasksController> mRecentTasksOptional;
 
     private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
     private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener();
-    private final FullscreenUnfoldController mFullscreenUnfoldController;
 
     public FullscreenTaskListener(SyncTransactionQueue syncQueue,
             Optional<FullscreenUnfoldController> unfoldController) {
+        this(syncQueue, unfoldController, Optional.empty());
+    }
+
+    public FullscreenTaskListener(SyncTransactionQueue syncQueue,
+            Optional<FullscreenUnfoldController> unfoldController,
+            Optional<RecentTasksController> recentTasks) {
         mSyncQueue = syncQueue;
         mFullscreenUnfoldController = unfoldController.orElse(null);
+        mRecentTasksOptional = recentTasks;
     }
 
     @Override
@@ -79,6 +88,7 @@
         });
 
         mAnimatableTasksListener.onTaskAppeared(taskInfo);
+        updateRecentsForVisibleFullscreenTask(taskInfo);
     }
 
     @Override
@@ -86,6 +96,7 @@
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
 
         mAnimatableTasksListener.onTaskInfoChanged(taskInfo);
+        updateRecentsForVisibleFullscreenTask(taskInfo);
 
         final TaskData data = mDataByTaskId.get(taskInfo.taskId);
         final Point positionInParent = taskInfo.positionInParent;
@@ -111,6 +122,15 @@
                 taskInfo.taskId);
     }
 
+    private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) {
+        mRecentTasksOptional.ifPresent(recentTasks -> {
+            if (taskInfo.isVisible) {
+                // Remove any persisted splits if either tasks are now made fullscreen and visible
+                recentTasks.removeSplitPair(taskInfo.taskId);
+            }
+        });
+    }
+
     @Override
     public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
         if (!mDataByTaskId.contains(taskId)) {
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 05f33b6..aeeb73f 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
@@ -490,12 +490,25 @@
         if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) {
             // Calculate the snap fraction of the current stack along the old movement bounds
             final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm();
-            final Rect postChangeStackBounds = new Rect(mPipBoundsState.getBounds());
-            final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds,
-                    mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds),
+            final float snapFraction = pipSnapAlgorithm.getSnapFraction(mPipBoundsState.getBounds(),
+                    mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds()),
                     mPipBoundsState.getStashedState());
 
             updateDisplayLayout.run();
+            final Rect postChangeStackBounds;
+            if (mPipBoundsState.getBounds() != null
+                    && (mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x
+                    || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y)) {
+                postChangeStackBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
+                        mPipBoundsState.getMaxSize().y);
+            } else if (mPipBoundsState.getBounds() != null
+                    && (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x
+                    || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y)) {
+                postChangeStackBounds = new Rect(0, 0, mPipBoundsState.getMinSize().x,
+                        mPipBoundsState.getMinSize().y);
+            } else {
+                postChangeStackBounds = new Rect(mPipBoundsState.getBounds());
+            }
 
             // Calculate the stack bounds in the new orientation based on same fraction along the
             // rotated movement bounds.
@@ -507,7 +520,7 @@
                     mPipBoundsState.getDisplayBounds(),
                     mPipBoundsState.getDisplayLayout().stableInsets());
 
-            mTouchHandler.getMotionHelper().movePip(postChangeStackBounds);
+            mTouchHandler.getMotionHelper().animateResizedBounds(postChangeStackBounds);
         } else {
             updateDisplayLayout.run();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 96fd59f..c634b7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -69,6 +69,7 @@
     private static final int UNSTASH_DURATION = 250;
     private static final int LEAVE_PIP_DURATION = 300;
     private static final int SHIFT_DURATION = 300;
+    private static final int ANIMATE_PIP_RESIZE_ANIMATION = 250;
 
     /** Friction to use for PIP when it moves via physics fling animations. */
     private static final float DEFAULT_FRICTION = 1.9f;
@@ -548,6 +549,14 @@
     }
 
     /**
+     * Animates the PiP from an old bound to a new bound. This is mostly used when display
+     * has changed and PiP bounds needs to be changed.
+     */
+    void animateResizedBounds(Rect newBounds) {
+        resizeAndAnimatePipUnchecked(newBounds, ANIMATE_PIP_RESIZE_ANIMATION);
+    }
+
+    /**
      * Animates the PiP to offset it from the IME or shelf.
      */
     void animateToOffset(Rect originalBounds, int offset) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index dbd0ff8..7457be2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -48,11 +48,11 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.BinderThread;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceId;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -66,10 +66,13 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -87,6 +90,29 @@
         RemoteCallable<SplitScreenController> {
     private static final String TAG = SplitScreenController.class.getSimpleName();
 
+    static final int EXIT_REASON_UNKNOWN = 0;
+    static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1;
+    static final int EXIT_REASON_APP_FINISHED = 2;
+    static final int EXIT_REASON_DEVICE_FOLDED = 3;
+    static final int EXIT_REASON_DRAG_DIVIDER = 4;
+    static final int EXIT_REASON_RETURN_HOME = 5;
+    static final int EXIT_REASON_ROOT_TASK_VANISHED = 6;
+    static final int EXIT_REASON_SCREEN_LOCKED = 7;
+    static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
+    @IntDef(value = {
+            EXIT_REASON_UNKNOWN,
+            EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
+            EXIT_REASON_APP_FINISHED,
+            EXIT_REASON_DEVICE_FOLDED,
+            EXIT_REASON_DRAG_DIVIDER,
+            EXIT_REASON_RETURN_HOME,
+            EXIT_REASON_ROOT_TASK_VANISHED,
+            EXIT_REASON_SCREEN_LOCKED,
+            EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ExitReason{}
+
     private final ShellTaskOrganizer mTaskOrganizer;
     private final SyncTransactionQueue mSyncQueue;
     private final Context mContext;
@@ -99,16 +125,31 @@
     private final TransactionPool mTransactionPool;
     private final SplitscreenEventLogger mLogger;
     private final IconProvider mIconProvider;
+    private final Optional<RecentTasksController> mRecentTasksOptional;
     private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
 
     private StageCoordinator mStageCoordinator;
 
+    // TODO(b/205019015): Remove after we clean up downstream modules
+    public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue, Context context,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            ShellExecutor mainExecutor, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        this(shellTaskOrganizer, syncQueue, context, rootTDAOrganizer, mainExecutor,
+                displayImeController, displayInsetsController, transitions, transactionPool,
+                iconProvider, Optional.empty(), unfoldControllerProvider);
+    }
+
     public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue, Context context,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer,
             ShellExecutor mainExecutor, DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
             Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
@@ -122,6 +163,7 @@
         mUnfoldControllerProvider = unfoldControllerProvider;
         mLogger = new SplitscreenEventLogger();
         mIconProvider = iconProvider;
+        mRecentTasksOptional = recentTasks;
     }
 
     public SplitScreen asSplitScreen() {
@@ -144,7 +186,7 @@
             mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                     mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
                     mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
-                    mIconProvider, mUnfoldControllerProvider);
+                    mIconProvider, mRecentTasksOptional, mUnfoldControllerProvider);
         }
     }
 
@@ -201,7 +243,7 @@
                 leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT, wct);
     }
 
-    public void exitSplitScreen(int toTopTaskId, int exitReason) {
+    public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
         mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
     }
 
@@ -348,6 +390,34 @@
         mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
     }
 
+    /**
+     * Return the {@param exitReason} as a string.
+     */
+    public static String exitReasonToString(int exitReason) {
+        switch (exitReason) {
+            case EXIT_REASON_UNKNOWN:
+                return "UNKNOWN_EXIT";
+            case EXIT_REASON_DRAG_DIVIDER:
+                return "DRAG_DIVIDER";
+            case EXIT_REASON_RETURN_HOME:
+                return "RETURN_HOME";
+            case EXIT_REASON_SCREEN_LOCKED:
+                return "SCREEN_LOCKED";
+            case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
+                return "SCREEN_LOCKED_SHOW_ON_TOP";
+            case EXIT_REASON_DEVICE_FOLDED:
+                return "DEVICE_FOLDED";
+            case EXIT_REASON_ROOT_TASK_VANISHED:
+                return "ROOT_TASK_VANISHED";
+            case EXIT_REASON_APP_FINISHED:
+                return "APP_FINISHED";
+            case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
+                return "APP_DOES_NOT_SUPPORT_MULTIWINDOW";
+            default:
+                return "unknown reason, reason int = " + exitReason;
+        }
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         pw.println(prefix + TAG);
         if (mStageCoordinator != null) {
@@ -497,8 +567,7 @@
         public void exitSplitScreen(int toTopTaskId) {
             executeRemoteCallWithTaskPermission(mController, "exitSplitScreen",
                     (controller) -> {
-                        controller.exitSplitScreen(toTopTaskId,
-                                FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT);
+                        controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN);
                     });
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 319079b..e320c2a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -17,13 +17,34 @@
 package com.android.wm.shell.splitscreen;
 
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN;
+
+import android.util.Slog;
 
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
+import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
 
 /**
  * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent
@@ -96,10 +117,40 @@
     }
 
     /**
+     * Returns the framework logging constant given a splitscreen exit reason.
+     */
+    private int getLoggerExitReason(@ExitReason int exitReason) {
+        switch (exitReason) {
+            case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+            case EXIT_REASON_APP_FINISHED:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+            case EXIT_REASON_DEVICE_FOLDED:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
+            case EXIT_REASON_DRAG_DIVIDER:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+            case EXIT_REASON_RETURN_HOME:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
+            case EXIT_REASON_ROOT_TASK_VANISHED:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
+            case EXIT_REASON_SCREEN_LOCKED:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
+            case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+            case EXIT_REASON_UNKNOWN:
+                // Fall through
+            default:
+                Slog.e("SplitscreenEventLogger", "Unknown exit reason: " + exitReason);
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
+        }
+    }
+
+    /**
      * Logs when the user exits splitscreen.  Only one of the main or side stages should be
      * specified to indicate which position was focused as a part of exiting (both can be unset).
      */
-    public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid,
+    public void logExit(@ExitReason int exitReason,
+            @SplitPosition int mainStagePosition, int mainStageUid,
             @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) {
         if (mLoggerSessionId == null) {
             // Ignore changes until we've started logging the session
@@ -113,7 +164,7 @@
         FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED,
                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT,
                 0 /* enterReason */,
-                exitReason,
+                getLoggerExitReason(exitReason),
                 0f /* splitRatio */,
                 getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape),
                 mainStageUid,
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 a94a081..3c35e6a 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
@@ -17,6 +17,7 @@
 package com.android.wm.shell.splitscreen;
 
 import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_OPEN;
@@ -25,15 +26,6 @@
 import static android.view.WindowManager.transitTypeToString;
 import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER;
 
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
-import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED;
@@ -41,6 +33,13 @@
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
+import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString;
 import static com.android.wm.shell.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP;
@@ -94,6 +93,8 @@
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.common.split.SplitWindowManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -148,6 +149,10 @@
     private final DisplayInsetsController mDisplayInsetsController;
     private final SplitScreenTransitions mSplitTransitions;
     private final SplitscreenEventLogger mLogger;
+    private final Optional<RecentTasksController> mRecentTasks;
+    // Tracks whether we should update the recent tasks.  Only allow this to happen in between enter
+    // and exit, since exit itself can trigger a number of changes that update the stages.
+    private boolean mShouldUpdateRecents;
     private boolean mExitSplitScreenOnHide;
     private boolean mKeyguardOccluded;
 
@@ -192,6 +197,7 @@
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, SplitscreenEventLogger logger,
             IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
@@ -199,6 +205,7 @@
         mRootTDAOrganizer = rootTDAOrganizer;
         mTaskOrganizer = taskOrganizer;
         mLogger = logger;
+        mRecentTasks = recentTasks;
         mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
         mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
 
@@ -239,6 +246,7 @@
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
             Transitions transitions, TransactionPool transactionPool,
             SplitscreenEventLogger logger,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
@@ -256,6 +264,7 @@
         mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
         mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
         mLogger = logger;
+        mRecentTasks = recentTasks;
         transitions.addHandler(this);
     }
 
@@ -527,7 +536,7 @@
         if (!showing && mMainStage.isActive()
                 && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
             exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
-                    SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED);
+                    EXIT_REASON_DEVICE_FOLDED);
         }
     }
 
@@ -535,7 +544,7 @@
         mExitSplitScreenOnHide = exitSplitScreenOnHide;
     }
 
-    void exitSplitScreen(int toTopTaskId, int exitReason) {
+    void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
         if (!mMainStage.isActive()) return;
 
         StageTaskListener childrenToTop = null;
@@ -552,7 +561,7 @@
         applyExitSplitScreen(childrenToTop, wct, exitReason);
     }
 
-    private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) {
+    private void exitSplitScreen(StageTaskListener childrenToTop, @ExitReason int exitReason) {
         if (!mMainStage.isActive()) return;
 
         final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -560,18 +569,30 @@
     }
 
     private void applyExitSplitScreen(StageTaskListener childrenToTop,
-            WindowContainerTransaction wct, int exitReason) {
+            WindowContainerTransaction wct, @ExitReason int exitReason) {
+        mRecentTasks.ifPresent(recentTasks -> {
+            // Notify recents if we are exiting in a way that breaks the pair, and disable further
+            // updates to splits in the recents until we enter split again
+            if (shouldBreakPairedTaskInRecents(exitReason) && mShouldUpdateRecents) {
+                recentTasks.removeSplitPair(mMainStage.getTopVisibleChildTaskId());
+                recentTasks.removeSplitPair(mSideStage.getTopVisibleChildTaskId());
+            }
+        });
+        mShouldUpdateRecents = false;
+
         mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
         mMainStage.deactivate(wct, childrenToTop == mMainStage);
         mTaskOrganizer.applyTransaction(wct);
         mSyncQueue.runInSync(t -> t
                 .setWindowCrop(mMainStage.mRootLeash, null)
                 .setWindowCrop(mSideStage.mRootLeash, null));
+
         // Hide divider and reset its position.
         setDividerVisibility(false);
         mSplitLayout.resetDividerPosition();
         mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
         Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason));
+        // Log the exit
         if (childrenToTop != null) {
             logExitToStage(exitReason, childrenToTop == mMainStage);
         } else {
@@ -580,6 +601,23 @@
     }
 
     /**
+     * Returns whether the split pair in the recent tasks list should be broken.
+     */
+    private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) {
+        switch (exitReason) {
+            // One of the apps doesn't support MW
+            case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
+            // User has explicitly dragged the divider to dismiss split
+            case EXIT_REASON_DRAG_DIVIDER:
+            // Either of the split apps have finished
+            case EXIT_REASON_APP_FINISHED:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
      * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
      * an existing WindowContainerTransaction (rather than applying immediately). This is intended
      * to be used when exiting split might be bundled with other window operations.
@@ -645,12 +683,28 @@
             mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
                     mSplitLayout.isLandscape());
         }
+        updateRecentTasksSplitPair();
 
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
         }
     }
 
+    private void updateRecentTasksSplitPair() {
+        if (!mShouldUpdateRecents) {
+            return;
+        }
+
+        mRecentTasks.ifPresent(recentTasks -> {
+            int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId();
+            int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId();
+            if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) {
+                // Update the pair for the top tasks
+                recentTasks.addSplitPair(mainStageTopTaskId, sideStageTopTaskId);
+            }
+        });
+    }
+
     private void sendSplitVisibilityChanged() {
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             final SplitScreen.SplitScreenListener l = mListeners.get(i);
@@ -724,8 +778,9 @@
                     // display, like the cases keyguard showing or screen off.
                     || (!mMainStage.mRootTaskInfo.isSleeping
                     && !mSideStage.mRootTaskInfo.isSleeping)) {
-                exitSplitScreen(null /* childrenToTop */,
-                        SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+            // Don't dismiss staged split when both stages are not visible due to sleeping display,
+            // like the cases keyguard showing or screen off.
+                exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
             }
         } else if (mKeyguardOccluded) {
             // At least one of the stages is visible while keyguard occluded. Dismiss split because
@@ -733,7 +788,7 @@
             // task contains show-when-locked activity remains on top after split dismissed.
             final StageTaskListener toTop =
                     mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null);
-            exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP);
+            exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
         }
 
         mSyncQueue.runInSync(t -> {
@@ -770,10 +825,10 @@
         if (!hasChildren) {
             if (isSideStage && mMainStageListener.mVisible) {
                 // Exit to main stage if side stage no longer has children.
-                exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+                exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
             } else if (!isSideStage && mSideStageListener.mVisible) {
                 // Exit to side stage if main stage no longer has children.
-                exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED);
+                exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
             }
         } else if (isSideStage) {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -783,12 +838,16 @@
             mSyncQueue.queue(wct);
             mSyncQueue.runInSync(t -> updateSurfaceBounds(mSplitLayout, t));
         }
-        if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
-                && mSideStageListener.mHasChildren) {
-            mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
-                    getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                    mSplitLayout.isLandscape());
+        if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
+            mShouldUpdateRecents = true;
+            updateRecentTasksSplitPair();
+
+            if (!mLogger.hasStartedSession()) {
+                mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+                        getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                        getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                        mSplitLayout.isLandscape());
+            }
         }
     }
 
@@ -808,8 +867,7 @@
             onSnappedToDismissTransition(mainStageToTop);
             return;
         }
-        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage,
-                SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER);
+        exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER);
     }
 
     @Override
@@ -1267,7 +1325,7 @@
     /**
      * Logs the exit of splitscreen.
      */
-    private void logExit(int exitReason) {
+    private void logExit(@ExitReason int exitReason) {
         mLogger.logExit(exitReason,
                 SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
                 SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
@@ -1278,7 +1336,7 @@
      * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
      * executed.
      */
-    private void logExitToStage(int exitReason, boolean toMainStage) {
+    private void logExitToStage(@ExitReason int exitReason, boolean toMainStage) {
         mLogger.logExit(exitReason,
                 toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
                 toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
@@ -1287,31 +1345,6 @@
                 mSplitLayout.isLandscape());
     }
 
-    private String exitReasonToString(int exitReason) {
-        switch (exitReason) {
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT:
-                return "UNKNOWN_EXIT";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER:
-                return "DRAG_DIVIDER";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME:
-                return "RETURN_HOME";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED:
-                return "SCREEN_LOCKED";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP:
-                return "SCREEN_LOCKED_SHOW_ON_TOP";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED:
-                return "DEVICE_FOLDED";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED:
-                return "ROOT_TASK_VANISHED";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED:
-                return "APP_FINISHED";
-            case SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW:
-                return "APP_DOES_NOT_SUPPORT_MULTIWINDOW";
-            default:
-                return "unknown reason, reason int = " + exitReason;
-        }
-    }
-
     class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
         boolean mHasRootTask = false;
         boolean mVisible = false;
@@ -1352,7 +1385,7 @@
         public void onNoLongerSupportMultiWindow() {
             if (mMainStage.isActive()) {
                 StageCoordinator.this.exitSplitScreen(null /* childrenToTop */,
-                        SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW);
+                        EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
             }
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 5100c56..190006e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -113,6 +114,19 @@
     }
 
     /**
+     * Returns the top visible child task's id.
+     */
+    int getTopVisibleChildTaskId() {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
+            if (info.isVisible) {
+                return info.taskId;
+            }
+        }
+        return INVALID_TASK_ID;
+    }
+
+    /**
      * Returns the top activity uid for the top child task.
      */
     int getTopChildTaskUid() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
index c35d833..46fe201 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.recents.RecentTasksController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -56,6 +57,8 @@
     @Mock
     private FullscreenUnfoldController mUnfoldController;
     @Mock
+    private RecentTasksController mRecentTasksController;
+    @Mock
     private SurfaceControl mSurfaceControl;
 
     private Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
@@ -66,7 +69,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mFullscreenUnfoldController = Optional.of(mUnfoldController);
-        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController);
+        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController,
+                Optional.empty());
     }
 
     @Test
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 935f669..c2f58b8 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
@@ -191,7 +191,7 @@
         mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged(
                 displayId, new Configuration());
 
-        verify(mMockPipMotionHelper).movePip(any(Rect.class));
+        verify(mMockPipMotionHelper).animateResizedBounds(any(Rect.class));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index f90af23..aab1e3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
@@ -72,10 +73,11 @@
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
                 SplitscreenEventLogger logger,
+                Optional<RecentTasksController> recentTasks,
                 Provider<Optional<StageTaskUnfoldController>> unfoldController) {
             super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
                     sideStage, imeController, insetsController, splitLayout, transitions,
-                    transactionPool, logger, unfoldController);
+                    transactionPool, logger, recentTasks, unfoldController);
 
             // Prepare default TaskDisplayArea for testing.
             mDisplayAreaInfo = new DisplayAreaInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index d5dee82..1eae625 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -66,6 +66,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -120,8 +121,7 @@
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
-                mTransactionPool,
-                mLogger, Optional::empty);
+                mTransactionPool, mLogger, Optional.empty(), Optional::empty);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
                 .when(mTransitions).startTransition(anyInt(), any(), any());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index e71fa94..ad65c04 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -22,6 +22,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -48,6 +49,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -162,8 +164,7 @@
     @Test
     public void testExitSplitScreen() {
         when(mMainStage.isActive()).thenReturn(true);
-        mStageCoordinator.exitSplitScreen(INVALID_TASK_ID,
-                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        mStageCoordinator.exitSplitScreen(INVALID_TASK_ID, EXIT_REASON_RETURN_HOME);
         verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
         verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false));
     }
@@ -174,8 +175,7 @@
         final int testTaskId = 12345;
         when(mMainStage.containsTask(eq(testTaskId))).thenReturn(true);
         when(mSideStage.containsTask(eq(testTaskId))).thenReturn(false);
-        mStageCoordinator.exitSplitScreen(testTaskId,
-                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
         verify(mMainStage).reorderChild(eq(testTaskId), eq(true),
                 any(WindowContainerTransaction.class));
         verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false));
@@ -188,8 +188,7 @@
         final int testTaskId = 12345;
         when(mMainStage.containsTask(eq(testTaskId))).thenReturn(false);
         when(mSideStage.containsTask(eq(testTaskId))).thenReturn(true);
-        mStageCoordinator.exitSplitScreen(testTaskId,
-                SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME);
+        mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME);
         verify(mSideStage).reorderChild(eq(testTaskId), eq(true),
                 any(WindowContainerTransaction.class));
         verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true));
@@ -200,7 +199,8 @@
         return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
                 mDisplayImeController, mDisplayInsetsController, splitLayout,
-                mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider());
+                mTransitions, mTransactionPool, mLogger, Optional.empty(),
+                new UnfoldControllerProvider());
     }
 
     private class UnfoldControllerProvider implements
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 71cd368..c6cc077 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -55,12 +55,10 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -3548,20 +3546,7 @@
      * whether sounds are heard or not.
      */
     public void playSoundEffect(@SystemSoundEffect int effectType) {
-        if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
-            return;
-        }
-
-        if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
-            return;
-        }
-
-        final IAudioService service = getService();
-        try {
-            service.playSoundEffect(effectType);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
-        }
+        playSoundEffect(effectType, UserHandle.USER_CURRENT);
     }
 
     /**
@@ -3577,13 +3562,9 @@
             return;
         }
 
-        if (!querySoundEffectsEnabled(userId)) {
-            return;
-        }
-
         final IAudioService service = getService();
         try {
-            service.playSoundEffect(effectType);
+            service.playSoundEffect(effectType, userId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -3612,14 +3593,6 @@
     }
 
     /**
-     * Settings has an in memory cache, so this is fast.
-     */
-    private boolean querySoundEffectsEnabled(int user) {
-        return Settings.System.getIntForUser(getContext().getContentResolver(),
-                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
-    }
-
-    /**
      *  Load Sound effects.
      *  This method must be called when sound effects are enabled.
      */
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index d736c25..6f03895 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -158,7 +158,7 @@
 
     int getMode();
 
-    oneway void playSoundEffect(int effectType);
+    oneway void playSoundEffect(int effectType, int userId);
 
     oneway void playSoundEffectVolume(int effectType, float volume);
 
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index c666255..73a821e 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -510,10 +510,10 @@
                     if (res != Tuner.RESULT_SUCCESS) {
                         TunerUtils.throwExceptionForResult(res, "failed to close frontend");
                     }
+                    mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
                 }
                 mIsSharedFrontend = false;
             }
-            mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
             FrameworkStatsLog
                     .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                     FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__UNKNOWN);
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
index cb41743..1b62d1a 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
@@ -130,7 +130,7 @@
         mContext = InstrumentationRegistry.getTargetContext();
         mMaxSignalLevel = mContext.getSystemService(WifiManager.class).getMaxSignalLevel();
         mWifiInfo = new WifiInfo();
-        mWifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(TEST_SSID));
+        mWifiInfo.setSSID(WifiSsid.fromString(TEST_SSID));
         mWifiInfo.setBSSID(TEST_BSSID);
         mScanResults = buildScanResultCache(TEST_SSID);
         mRoamingScans = buildScanResultCache(ROAMING_SSID);
@@ -318,7 +318,7 @@
                 new NetworkInfo(ConnectivityManager.TYPE_WIFI, 2, "WIFI", "WIFI_SUBTYPE");
         AccessPoint accessPoint = new AccessPoint(mContext, configuration);
         WifiInfo wifiInfo = new WifiInfo();
-        wifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(configuration.SSID));
+        wifiInfo.setSSID(WifiSsid.fromString(configuration.SSID));
         wifiInfo.setBSSID(configuration.BSSID);
         wifiInfo.setNetworkId(configuration.networkId);
         accessPoint.update(configuration, wifiInfo, networkInfo);
@@ -334,7 +334,7 @@
                 new NetworkInfo(ConnectivityManager.TYPE_WIFI, 2, "WIFI", "WIFI_SUBTYPE");
         AccessPoint accessPoint = new AccessPoint(mContext, configuration);
         WifiInfo wifiInfo = new WifiInfo();
-        wifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(configuration.SSID));
+        wifiInfo.setSSID(WifiSsid.fromString(configuration.SSID));
         wifiInfo.setBSSID(configuration.BSSID);
         wifiInfo.setNetworkId(configuration.networkId);
         wifiInfo.setMeteredHint(true);
@@ -367,7 +367,7 @@
                 new NetworkInfo(ConnectivityManager.TYPE_WIFI, 2, "WIFI", "WIFI_SUBTYPE");
         AccessPoint accessPoint = new AccessPoint(mContext, configuration);
         WifiInfo wifiInfo = new WifiInfo();
-        wifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(configuration.SSID));
+        wifiInfo.setSSID(WifiSsid.fromString(configuration.SSID));
         wifiInfo.setBSSID(configuration.BSSID);
         wifiInfo.setNetworkId(configuration.networkId);
         accessPoint.update(configuration, wifiInfo, networkInfo);
@@ -518,7 +518,7 @@
         final String connectedViaAppResourceString = "Connected via ";
 
         WifiInfo wifiInfo = new WifiInfo();
-        wifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(TEST_SSID));
+        wifiInfo.setSSID(WifiSsid.fromString(TEST_SSID));
         wifiInfo.setEphemeral(true);
         wifiInfo.setRequestingPackageName(appPackageName);
         wifiInfo.setRssi(rssi);
@@ -585,7 +585,7 @@
     private WifiConfiguration createWifiConfiguration() {
         WifiConfiguration configuration = new WifiConfiguration();
         configuration.BSSID = "bssid";
-        configuration.SSID = "ssid";
+        configuration.SSID = "\"ssid\"";
         configuration.networkId = 123;
         return configuration;
     }
@@ -1024,7 +1024,7 @@
 
         WifiInfo info = new WifiInfo();
         info.setRssi(DEFAULT_RSSI);
-        info.setSSID(WifiSsid.createFromAsciiEncoded(TEST_SSID));
+        info.setSSID(WifiSsid.fromString(TEST_SSID));
         info.setBSSID(bssid);
         info.setNetworkId(NETWORK_ID);
 
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java
index b0a9136..e7533bc1 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/WifiTrackerTest.java
@@ -124,7 +124,7 @@
     private static final int CONNECTED_RSSI = -50;
     private static final WifiInfo CONNECTED_AP_1_INFO = new WifiInfo();
     static {
-        CONNECTED_AP_1_INFO.setSSID(WifiSsid.createFromAsciiEncoded(SSID_1));
+        CONNECTED_AP_1_INFO.setSSID(WifiSsid.fromUtf8Text(SSID_1));
         CONNECTED_AP_1_INFO.setBSSID(BSSID_1);
         CONNECTED_AP_1_INFO.setNetworkId(NETWORK_ID_1);
         CONNECTED_AP_1_INFO.setRssi(CONNECTED_RSSI);
@@ -246,7 +246,7 @@
 
     private static ScanResult buildScanResult1() {
         return new ScanResult(
-                WifiSsid.createFromAsciiEncoded(SSID_1),
+                WifiSsid.fromUtf8Text(SSID_1),
                 BSSID_1,
                 0, // hessid
                 0, //anqpDomainId
@@ -259,7 +259,7 @@
 
     private static ScanResult buildScanResult2() {
         return new ScanResult(
-                WifiSsid.createFromAsciiEncoded(SSID_2),
+                WifiSsid.fromUtf8Text(SSID_2),
                 BSSID_2,
                 0, // hessid
                 0, //anqpDomainId
@@ -272,7 +272,7 @@
 
     private static ScanResult buildScanResultWithTimestamp(long timestampMillis) {
         return new ScanResult(
-                WifiSsid.createFromAsciiEncoded(SSID_3),
+                WifiSsid.fromUtf8Text(SSID_3),
                 BSSID_3,
                 0, // hessid
                 0, //anqpDomainId
@@ -444,7 +444,7 @@
         networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTING, "connecting", "test");
 
         WifiInfo info = new WifiInfo();
-        info.setSSID(WifiSsid.createFromAsciiEncoded(SSID_2));
+        info.setSSID(WifiSsid.fromUtf8Text(SSID_2));
         info.setBSSID(BSSID_2);
         info.setRssi(CONNECTED_RSSI);
         info.setNetworkId(NETWORK_ID_2);
@@ -671,7 +671,7 @@
         String ssid = "ssid3";
         String bssid = "00:00:00:00:00:00";
         ScanResult newResult = new ScanResult(
-                WifiSsid.createFromAsciiEncoded(ssid),
+                WifiSsid.fromUtf8Text(ssid),
                 bssid,
                 0, // hessid
                 0, //anqpDomainId
@@ -720,7 +720,7 @@
         // Add a Passpoint AP to the scan results.
         List<ScanResult> results = new ArrayList<>();
         ScanResult passpointAp = new ScanResult(
-                WifiSsid.createFromAsciiEncoded(SSID_1),
+                WifiSsid.fromUtf8Text(SSID_1),
                 BSSID_1,
                 0, // hessid
                 0, //anqpDomainId
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index ddf0289..b620654 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -162,6 +162,7 @@
     <uses-permission android:name="android.permission.SET_ORIENTATION" />
     <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.INSTALL_PACKAGE_UPDATES" />
+    <uses-permission android:name="android.permission.INSTALL_DPC_PACKAGES" />
     <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" />
     <uses-permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE" />
     <uses-permission android:name="com.android.permission.USE_SYSTEM_DATA_LOADERS" />
@@ -200,6 +201,7 @@
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.CREATE_USERS" />
+    <uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
     <uses-permission android:name="android.permission.MANAGE_CREDENTIAL_MANAGEMENT_APP" />
     <uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
     <uses-permission android:name="android.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS" />
@@ -654,6 +656,15 @@
             </intent-filter>
         </receiver>
 
+        <receiver
+            android:name=".ProfcollectUploadReceiver"
+            android:exported="true"
+            android:permission="android.permission.TRIGGER_SHELL_PROFCOLLECT_UPLOAD" >
+            <intent-filter>
+                <action android:name="com.android.shell.action.PROFCOLLECT_UPLOAD" />
+            </intent-filter>
+        </receiver>
+
         <service
             android:name=".BugreportProgressService"
             android:exported="false"/>
diff --git a/packages/Shell/res/xml/file_provider_paths.xml b/packages/Shell/res/xml/file_provider_paths.xml
index 225c757..85d7dd3 100644
--- a/packages/Shell/res/xml/file_provider_paths.xml
+++ b/packages/Shell/res/xml/file_provider_paths.xml
@@ -1,3 +1,4 @@
 <paths xmlns:android="http://schemas.android.com/apk/res/android">
     <files-path name="bugreports" path="bugreports/" />
+    <root-path name="profcollect" path="/data/misc/profcollectd/report/" />
 </paths>
diff --git a/packages/Shell/src/com/android/shell/ProfcollectUploadReceiver.java b/packages/Shell/src/com/android/shell/ProfcollectUploadReceiver.java
new file mode 100644
index 0000000..d2da724
--- /dev/null
+++ b/packages/Shell/src/com/android/shell/ProfcollectUploadReceiver.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.shell;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.core.content.FileProvider;
+
+import com.android.internal.R;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * A proxy service that relays report upload requests to the uploader app, while translating
+ * the path to the report to a content URI owned by this service.
+ */
+public final class ProfcollectUploadReceiver extends BroadcastReceiver {
+    private static final String AUTHORITY = "com.android.shell";
+    private static final String PROFCOLLECT_DATA_ROOT = "/data/misc/profcollectd/report/";
+
+    private static final String LOG_TAG = "ProfcollectUploadReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.i(LOG_TAG, "Received upload intent");
+
+        String uploaderPkg = getUploaderPackageName(context);
+        String uploaderAction = getUploaderActionName(context);
+
+        try {
+            ApplicationInfo info = context.getPackageManager().getApplicationInfo(uploaderPkg,
+                    0);
+            if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+                Log.e(LOG_TAG, "The profcollect uploader app " + uploaderPkg
+                        + " must be a system application");
+                return;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(LOG_TAG, "Cannot find profcollect uploader app " + uploaderPkg);
+            return;
+        }
+
+        String filename = intent.getStringExtra("filename");
+        File reportFile = new File(PROFCOLLECT_DATA_ROOT + filename);
+        Uri reportUri = FileProvider.getUriForFile(context, AUTHORITY, reportFile);
+        Intent uploadIntent =
+                new Intent(uploaderAction)
+                        .setPackage(uploaderPkg)
+                        .putExtra("EXTRA_DESTINATION", "PROFCOLLECT")
+                        .putExtra("EXTRA_PACKAGE_NAME", context.getPackageName())
+                        .setData(reportUri)
+                        .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+
+        List<ResolveInfo> receivers =
+                context.getPackageManager().queryBroadcastReceivers(uploadIntent, 0);
+        if (receivers == null || receivers.isEmpty()) {
+            Log.e(LOG_TAG, "No one to receive upload intent, abort upload.");
+            return;
+        }
+
+        context.grantUriPermission(uploaderPkg, reportUri,
+                Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        context.sendBroadcast(uploadIntent);
+    }
+
+    private String getUploaderPackageName(Context context) {
+        return context.getResources().getString(
+                R.string.config_defaultProfcollectReportUploaderApp);
+    }
+
+    private String getUploaderActionName(Context context) {
+        return context.getResources().getString(
+                R.string.config_defaultProfcollectReportUploaderAction);
+    }
+}
diff --git a/packages/SystemUI/res/drawable/udfps_enroll_checkmark.xml b/packages/SystemUI/res/drawable/udfps_enroll_checkmark.xml
new file mode 100644
index 0000000..f8169d3
--- /dev/null
+++ b/packages/SystemUI/res/drawable/udfps_enroll_checkmark.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="54dp"
+        android:height="54dp"
+        android:viewportWidth="54"
+        android:viewportHeight="54">
+    <path
+        android:pathData="M26.9999,3.9619C39.7029,3.9619 50.0369,14.2969 50.0369,26.9999C50.0369,39.7029 39.7029,50.0379 26.9999,50.0379C14.2969,50.0379 3.9629,39.7029 3.9629,26.9999C3.9629,14.2969 14.2969,3.9619 26.9999,3.9619Z"
+        android:fillColor="?android:colorBackground"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M27,0C12.088,0 0,12.088 0,27C0,41.912 12.088,54 27,54C41.912,54 54,41.912 54,27C54,12.088 41.912,0 27,0ZM27,3.962C39.703,3.962 50.037,14.297 50.037,27C50.037,39.703 39.703,50.038 27,50.038C14.297,50.038 3.963,39.703 3.963,27C3.963,14.297 14.297,3.962 27,3.962Z"
+        android:fillColor="@color/udfps_enroll_progress"
+        android:fillType="evenOdd"/>
+    <path
+        android:pathData="M23.0899,38.8534L10.4199,26.1824L13.2479,23.3544L23.0899,33.1974L41.2389,15.0474L44.0679,17.8754L23.0899,38.8534Z"
+        android:fillColor="@color/udfps_enroll_progress"
+        android:fillType="evenOdd"/>
+</vector>
diff --git a/packages/SystemUI/res/xml/media_collapsed.xml b/packages/SystemUI/res/xml/media_collapsed.xml
index c3510b6..12e446f 100644
--- a/packages/SystemUI/res/xml/media_collapsed.xml
+++ b/packages/SystemUI/res/xml/media_collapsed.xml
@@ -58,7 +58,7 @@
     <!-- Song name -->
     <Constraint
         android:id="@+id/header_title"
-        android:layout_width="0dp"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/qs_media_info_margin"
         android:layout_marginEnd="@dimen/qs_center_guideline_padding"
@@ -71,7 +71,7 @@
     <!-- Artist name -->
     <Constraint
         android:id="@+id/header_artist"
-        android:layout_width="0dp"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         app:layout_constrainedWidth="true"
         android:layout_marginTop="@dimen/qs_media_info_spacing"
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 25db478..2909043 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -62,6 +62,7 @@
     srcs: [
         "src/com/android/systemui/flags/Flag.kt",
     ],
+    include_srcs: true,
     static_kotlin_stdlib: false,
     java_version: "1.8",
     min_sdk_version: "current",
@@ -74,11 +75,11 @@
     ],
     static_kotlin_stdlib: false,
     libs: [
+        "SystemUI-flags",
         "androidx.concurrent_concurrent-futures",
     ],
     static_libs: [
         "SystemUI-flag-types",
-        "SystemUI-flags",
     ],
     java_version: "1.8",
     min_sdk_version: "current",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
index 68834bc..d9b6a34 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
@@ -16,37 +16,157 @@
 
 package com.android.systemui.flags
 
-interface Flag<T> {
+import android.os.Parcel
+import android.os.Parcelable
+
+interface Flag<T> : Parcelable {
     val id: Int
     val default: T
+
+    override fun describeContents() = 0
 }
 
+// Consider using the "parcelize" kotlin library.
+
 data class BooleanFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: Boolean = false
-) : Flag<Boolean>
+) : Flag<Boolean> {
+
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<BooleanFlag> {
+            override fun createFromParcel(parcel: Parcel) = BooleanFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<BooleanFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readBoolean()
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeBoolean(default)
+    }
+}
 
 data class StringFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: String = ""
-) : Flag<String>
+) : Flag<String> {
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<StringFlag> {
+            override fun createFromParcel(parcel: Parcel) = StringFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<StringFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readString() ?: ""
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeString(default)
+    }
+}
 
 data class IntFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: Int = 0
-) : Flag<Int>
+) : Flag<Int> {
+
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<IntFlag> {
+            override fun createFromParcel(parcel: Parcel) = IntFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<IntFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readInt()
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeInt(default)
+    }
+}
 
 data class LongFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: Long = 0
-) : Flag<Long>
+) : Flag<Long> {
+
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<LongFlag> {
+            override fun createFromParcel(parcel: Parcel) = LongFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<LongFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readLong()
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeLong(default)
+    }
+}
 
 data class FloatFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: Float = 0f
-) : Flag<Float>
+) : Flag<Float> {
+
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<FloatFlag> {
+            override fun createFromParcel(parcel: Parcel) = FloatFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<FloatFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readFloat()
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeFloat(default)
+    }
+}
 
 data class DoubleFlag @JvmOverloads constructor(
     override val id: Int,
     override val default: Double = 0.0
-) : Flag<Double>
\ No newline at end of file
+) : Flag<Double> {
+
+    companion object {
+        @JvmField
+        val CREATOR = object : Parcelable.Creator<DoubleFlag> {
+            override fun createFromParcel(parcel: Parcel) = DoubleFlag(parcel)
+            override fun newArray(size: Int) = arrayOfNulls<DoubleFlag>(size)
+        }
+    }
+
+    private constructor(parcel: Parcel) : this(
+        id = parcel.readInt(),
+        default = parcel.readDouble()
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeInt(id)
+        parcel.writeDouble(default)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index cbb942b..1dc555e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -16,10 +16,13 @@
 
 package com.android.systemui.flags
 
+import android.app.Activity
+import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
 import android.database.ContentObserver
 import android.net.Uri
+import android.os.Bundle
 import android.os.Handler
 import android.provider.Settings
 import androidx.concurrent.futures.CallbackToFutureAdapter
@@ -34,10 +37,12 @@
     companion object {
         const val RECEIVING_PACKAGE = "com.android.systemui"
         const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
+        const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
         const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
         const val FIELD_ID = "id"
         const val FIELD_VALUE = "value"
         const val FIELD_TYPE = "type"
+        const val FIELD_FLAGS = "flags"
         const val TYPE_BOOLEAN = "boolean"
         private const val SETTINGS_PREFIX = "systemui/flags"
     }
@@ -46,14 +51,26 @@
     private val settingsObserver: ContentObserver = SettingsObserver()
 
     fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
-        val knownFlagMap = Flags.collectFlags()
-        // Possible todo in the future: query systemui async to actually get the known flag ids.
-        return CallbackToFutureAdapter.getFuture(
-            CallbackToFutureAdapter.Resolver {
-                completer: CallbackToFutureAdapter.Completer<Collection<Flag<*>>> ->
-                completer.set(knownFlagMap.values as Collection<Flag<*>>)
-                "Retrieving Flags"
-            })
+        val intent = Intent(ACTION_GET_FLAGS)
+        intent.setPackage(RECEIVING_PACKAGE)
+
+        return CallbackToFutureAdapter.getFuture {
+            completer: CallbackToFutureAdapter.Completer<Any?> ->
+                context.sendOrderedBroadcast(intent, null,
+                    object : BroadcastReceiver() {
+                        override fun onReceive(context: Context, intent: Intent) {
+                            val extras: Bundle? = getResultExtras(false)
+                            val listOfFlags: java.util.ArrayList<Flag<*>>? =
+                                extras?.getParcelableArrayList(FIELD_FLAGS)
+                            if (listOfFlags != null) {
+                                completer.set(listOfFlags)
+                            } else {
+                                completer.setException(NoFlagResultsException())
+                            }
+                        }
+                    }, null, Activity.RESULT_OK, "extra data", null)
+            "QueryingFlags"
+        } as ListenableFuture<Collection<Flag<*>>>
     }
 
     fun setFlagValue(id: Int, enabled: Boolean) {
@@ -149,4 +166,7 @@
     }
 }
 
-class InvalidFlagStorageException : Exception("Data found but is invalid")
\ No newline at end of file
+class InvalidFlagStorageException : Exception("Data found but is invalid")
+
+class NoFlagResultsException : Exception(
+    "SystemUI failed to communicate its flags back successfully")
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/ThumbnailData.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/ThumbnailData.java
index 6594d5f..2f2876d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/ThumbnailData.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/ThumbnailData.java
@@ -22,7 +22,6 @@
 
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_UNDEFINED;
 
-import android.window.TaskSnapshot;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Point;
@@ -30,6 +29,9 @@
 import android.hardware.HardwareBuffer;
 import android.util.Log;
 import android.view.WindowInsetsController.Appearance;
+import android.window.TaskSnapshot;
+
+import java.util.HashMap;
 
 /**
  * Data for a single thumbnail.
@@ -80,6 +82,18 @@
         return thumbnail;
     }
 
+    public static HashMap<Integer, ThumbnailData> wrap(int[] taskIds, TaskSnapshot[] snapshots) {
+        HashMap<Integer, ThumbnailData> temp = new HashMap<>();
+        if (taskIds == null || snapshots == null || taskIds.length != snapshots.length) {
+            return temp;
+        }
+
+        for (int i = snapshots.length - 1; i >= 0; i--) {
+            temp.put(taskIds[i], new ThumbnailData(snapshots[i]));
+        }
+        return temp;
+    }
+
     public ThumbnailData(TaskSnapshot snapshot) {
         thumbnail = makeThumbnail(snapshot);
         insets = new Rect(snapshot.getContentInsets());
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 9164137..b95123d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -28,7 +28,6 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RecentTaskInfo;
 import android.app.ActivityManager.RunningTaskInfo;
-import android.window.TaskSnapshot;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.AppGlobals;
@@ -50,6 +49,7 @@
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
 import android.view.RemoteAnimationTarget;
+import android.window.TaskSnapshot;
 
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.systemui.shared.recents.model.Task;
@@ -189,9 +189,9 @@
                     }
 
                     @Override
-                    public void onAnimationCanceled(TaskSnapshot taskSnapshot) {
+                    public void onAnimationCanceled(int[] taskIds, TaskSnapshot[] taskSnapshots) {
                         animationHandler.onAnimationCanceled(
-                                taskSnapshot != null ? new ThumbnailData(taskSnapshot) : null);
+                                ThumbnailData.wrap(taskIds, taskSnapshots));
                     }
 
                     @Override
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
index 8e65560..13f1db4a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -39,12 +39,14 @@
 
     public ThumbnailData screenshotTask(int taskId) {
         try {
-            TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
-            return snapshot != null ? new ThumbnailData(snapshot) : new ThumbnailData();
+            final TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
+            if (snapshot != null) {
+                return new ThumbnailData(snapshot);
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "Failed to screenshot task", e);
-            return new ThumbnailData();
         }
+        return new ThumbnailData();
     }
 
     public void setInputConsumerEnabled(boolean enabled) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index c4cd192..a74de2e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -20,6 +20,8 @@
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
+import java.util.HashMap;
+
 public interface RecentsAnimationListener {
     /**
      * Called when the animation into Recents can start. This call is made on the binder thread.
@@ -31,7 +33,7 @@
     /**
      * Called when the animation into Recents was canceled. This call is made on the binder thread.
      */
-    void onAnimationCanceled(ThumbnailData thumbnailData);
+    void onAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas);
 
     /**
      * Called when the task of an activity that has been started while the recents animation
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
index 9311966..ef04619 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FeatureFlagManager.java
@@ -16,10 +16,11 @@
 
 package com.android.systemui.flags;
 
+import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS;
 import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG;
+import static com.android.systemui.flags.FlagManager.FIELD_FLAGS;
 import static com.android.systemui.flags.FlagManager.FIELD_ID;
 import static com.android.systemui.flags.FlagManager.FIELD_VALUE;
-import static com.android.systemui.flags.FlagManager.FLAGS_PERMISSION;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -69,8 +70,10 @@
             DumpManager dumpManager) {
         mFlagManager = flagManager;
         mSecureSettings = secureSettings;
-        IntentFilter filter = new IntentFilter(ACTION_SET_FLAG);
-        context.registerReceiver(mReceiver, filter, FLAGS_PERMISSION, null);
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ACTION_SET_FLAG);
+        filter.addAction(ACTION_GET_FLAGS);
+        context.registerReceiver(mReceiver, filter, null, null);
         dumpManager.registerDumpable(TAG, this);
     }
 
@@ -151,9 +154,15 @@
             if (action == null) {
                 return;
             }
-
             if (ACTION_SET_FLAG.equals(action)) {
                 handleSetFlag(intent.getExtras());
+            } else if (ACTION_GET_FLAGS.equals(action)) {
+                Map<Integer, Flag<?>> knownFlagMap = Flags.collectFlags();
+                ArrayList<Flag<?>> flags = new ArrayList<>(knownFlagMap.values());
+                Bundle extras =  getResultExtras(true);
+                if (extras != null) {
+                    extras.putParcelableArrayList(FIELD_FLAGS, flags);
+                }
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
index dacc169..7bb4708 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
@@ -17,6 +17,7 @@
 
 import android.animation.Animator
 import android.animation.AnimatorListenerAdapter
+import android.app.ActivityTaskManager
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.PorterDuff
@@ -24,6 +25,7 @@
 import android.graphics.Rect
 import android.hardware.biometrics.BiometricOverlayConstants
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
 import android.hardware.display.DisplayManager
 import android.hardware.fingerprint.FingerprintManager
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
@@ -61,6 +63,7 @@
     private val layoutInflater: LayoutInflater,
     fingerprintManager: FingerprintManager?,
     private val windowManager: WindowManager,
+    private val activityTaskManager: ActivityTaskManager,
     overviewProxyService: OverviewProxyService,
     displayManager: DisplayManager,
     @Main mainExecutor: DelayableExecutor,
@@ -130,7 +133,7 @@
             override fun show(
                 sensorId: Int,
                 @BiometricOverlayConstants.ShowReason reason: Int
-            ) = if (reason.isReasonToShow()) doShow() else hide(sensorId)
+            ) = if (reason.isReasonToShow(activityTaskManager)) doShow() else hide(sensorId)
 
             private fun doShow() = mainExecutor.execute {
                 if (overlayView == null) {
@@ -228,11 +231,19 @@
 }
 
 @BiometricOverlayConstants.ShowReason
-private fun Int.isReasonToShow(): Boolean = when (this) {
+private fun Int.isReasonToShow(activityTaskManager: ActivityTaskManager): Boolean = when (this) {
     REASON_AUTH_KEYGUARD -> false
+    REASON_AUTH_SETTINGS -> when (activityTaskManager.topClass()) {
+        // TODO(b/186176653): exclude fingerprint overlays from this list view
+        "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
+        else -> true
+    }
     else -> true
 }
 
+private fun ActivityTaskManager.topClass(): String =
+    getTasks(1).firstOrNull()?.topActivity?.className ?: ""
+
 @RawRes
 private fun Display.asSideFpsAnimation(): Int = when (rotation) {
     Surface.ROTATION_0 -> R.raw.sfps_pulse
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
index 70bc56b..9474340 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java
@@ -63,13 +63,13 @@
 
     @Override
     protected void onViewAttached() {
-        mPanelExpansionStateManager.addListener(mPanelExpansionListener);
+        mPanelExpansionStateManager.addExpansionListener(mPanelExpansionListener);
         mDumpManger.registerDumpable(getDumpTag(), this);
     }
 
     @Override
     protected void onViewDetached() {
-        mPanelExpansionStateManager.removeListener(mPanelExpansionListener);
+        mPanelExpansionStateManager.removeExpansionListener(mPanelExpansionListener);
         mDumpManger.unregisterDumpable(getDumpTag());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index b484029..a517ae2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -760,10 +760,12 @@
                 mOnFingerDown = false;
                 mView.setSensorProperties(mSensorProps);
                 mView.setHbmProvider(mHbmProvider);
-                UdfpsAnimationViewController animation = inflateUdfpsAnimation(reason);
+                UdfpsAnimationViewController<?> animation = inflateUdfpsAnimation(reason);
                 mAttemptedToDismissKeyguard = false;
-                animation.init();
-                mView.setAnimationViewController(animation);
+                if (animation != null) {
+                    animation.init();
+                    mView.setAnimationViewController(animation);
+                }
                 mOrientationListener.enable();
 
                 // This view overlaps the sensor area, so prevent it from being selectable
@@ -786,7 +788,8 @@
         }
     }
 
-    private UdfpsAnimationViewController inflateUdfpsAnimation(int reason) {
+    @Nullable
+    private UdfpsAnimationViewController<?> inflateUdfpsAnimation(int reason) {
         switch (reason) {
             case BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR:
             case BiometricOverlayConstants.REASON_ENROLL_ENROLLING:
@@ -830,6 +833,7 @@
                         mDumpManager
                 );
             case BiometricOverlayConstants.REASON_AUTH_OTHER:
+            case BiometricOverlayConstants.REASON_AUTH_SETTINGS:
                 UdfpsFpmOtherView authOtherView = (UdfpsFpmOtherView)
                         mInflater.inflate(R.layout.udfps_fpm_other_view, null);
                 mView.addView(authOtherView);
@@ -840,7 +844,7 @@
                         mDumpManager
                 );
             default:
-                Log.d(TAG, "Animation for reason " + reason + " not supported yet");
+                Log.e(TAG, "Animation for reason " + reason + " not supported yet");
                 return null;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
index 2034ff3..1f01fc5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java
@@ -115,7 +115,8 @@
         mBlueFill.setColor(context.getColor(R.color.udfps_moving_target_fill));
         mBlueFill.setStyle(Paint.Style.FILL);
 
-        mMovingTargetFpIcon = context.getResources().getDrawable(R.drawable.ic_fingerprint, null);
+        mMovingTargetFpIcon = context.getResources()
+                .getDrawable(R.drawable.ic_kg_fingerprint, null);
         mMovingTargetFpIcon.setTint(Color.WHITE);
         mMovingTargetFpIcon.mutate();
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index 11addf0..79c7e66 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -16,17 +16,22 @@
 
 package com.android.systemui.biometrics;
 
+import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.ColorFilter;
+import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
-import android.util.Log;
+import android.util.TypedValue;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
 
+import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import java.util.ArrayList;
-import java.util.List;
+import com.android.systemui.R;
 
 /**
  * UDFPS enrollment progress bar.
@@ -34,108 +39,193 @@
 public class UdfpsEnrollProgressBarDrawable extends Drawable {
     private static final String TAG = "UdfpsProgressBar";
 
-    private static final float SEGMENT_GAP_ANGLE = 12f;
+    private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
+    private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
+    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
+    private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
+    private static final float STROKE_WIDTH_DP = 12f;
 
-    @NonNull private final Context mContext;
+    private final float mStrokeWidthPx;
+    @ColorInt private final int mProgressColor;
+    @ColorInt private final int mHelpColor;
+    @NonNull private final Drawable mCheckmarkDrawable;
+    @NonNull private final Interpolator mCheckmarkInterpolator;
+    @NonNull private final Paint mBackgroundPaint;
+    @NonNull private final Paint mFillPaint;
 
-    @Nullable private UdfpsEnrollHelper mEnrollHelper;
-    @NonNull private List<UdfpsEnrollProgressBarSegment> mSegments = new ArrayList<>();
+    private boolean mAfterFirstTouch;
+
+    private int mRemainingSteps = 0;
     private int mTotalSteps = 0;
-    private int mProgressSteps = 0;
-    private boolean mIsShowingHelp = false;
+    private float mProgress = 0f;
+    @Nullable private ValueAnimator mProgressAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
+
+    private boolean mShowingHelp = false;
+    @Nullable private ValueAnimator mFillColorAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
+
+    private boolean mComplete = false;
+    private float mCheckmarkScale = 0f;
+    @Nullable private ValueAnimator mCheckmarkAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener;
 
     public UdfpsEnrollProgressBarDrawable(@NonNull Context context) {
-        mContext = context;
-    }
+        mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
+        mProgressColor = context.getColor(R.color.udfps_enroll_progress);
+        mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
+        mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
+        mCheckmarkDrawable.mutate();
+        mCheckmarkInterpolator = new OvershootInterpolator();
 
-    void setEnrollHelper(@Nullable UdfpsEnrollHelper enrollHelper) {
-        mEnrollHelper = enrollHelper;
-        if (enrollHelper != null) {
-            final int stageCount = enrollHelper.getStageCount();
-            mSegments = new ArrayList<>(stageCount);
-            float startAngle = SEGMENT_GAP_ANGLE / 2f;
-            final float sweepAngle = (360f / stageCount) - SEGMENT_GAP_ANGLE;
-            final Runnable invalidateRunnable = this::invalidateSelf;
-            for (int index = 0; index < stageCount; index++) {
-                mSegments.add(new UdfpsEnrollProgressBarSegment(mContext, getBounds(), startAngle,
-                        sweepAngle, SEGMENT_GAP_ANGLE, invalidateRunnable));
-                startAngle += sweepAngle + SEGMENT_GAP_ANGLE;
-            }
-            invalidateSelf();
+        mBackgroundPaint = new Paint();
+        mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
+        mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
+        mBackgroundPaint.setAntiAlias(true);
+        mBackgroundPaint.setStyle(Paint.Style.STROKE);
+        mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
+
+        // Set background paint color and alpha.
+        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
+        final TypedArray typedArray = context.obtainStyledAttributes(attrs);
+        try {
+            @ColorInt final int tintColor = typedArray.getColor(0, mBackgroundPaint.getColor());
+            mBackgroundPaint.setColor(tintColor);
+        } finally {
+            typedArray.recycle();
         }
+        TypedValue alpha = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
+        mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
+
+        // Progress fill should *not* use the extracted system color.
+        mFillPaint = new Paint();
+        mFillPaint.setStrokeWidth(mStrokeWidthPx);
+        mFillPaint.setColor(mProgressColor);
+        mFillPaint.setAntiAlias(true);
+        mFillPaint.setStyle(Paint.Style.STROKE);
+        mFillPaint.setStrokeCap(Paint.Cap.ROUND);
+
+        mProgressUpdateListener = animation -> {
+            mProgress = (float) animation.getAnimatedValue();
+            invalidateSelf();
+        };
+
+        mFillColorUpdateListener = animation -> {
+            mFillPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
+
+        mCheckmarkUpdateListener = animation -> {
+            mCheckmarkScale = (float) animation.getAnimatedValue();
+            invalidateSelf();
+        };
     }
 
     void onEnrollmentProgress(int remaining, int totalSteps) {
-        mTotalSteps = totalSteps;
-
-        // Show some progress for the initial touch.
-        updateState(Math.max(1, totalSteps - remaining), false /* isShowingHelp */);
+        mAfterFirstTouch = true;
+        updateState(remaining, totalSteps, false /* showingHelp */);
     }
 
     void onEnrollmentHelp(int remaining, int totalSteps) {
-        updateState(Math.max(0, totalSteps - remaining), true /* isShowingHelp */);
+        updateState(remaining, totalSteps, true /* showingHelp */);
     }
 
     void onLastStepAcquired() {
-        updateState(mTotalSteps, false /* isShowingHelp */);
+        updateState(0, mTotalSteps, false /* showingHelp */);
     }
 
-    private void updateState(int progressSteps, boolean isShowingHelp) {
-        updateProgress(progressSteps);
-        updateFillColor(isShowingHelp);
+    private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
+        updateProgress(remainingSteps, totalSteps);
+        updateFillColor(showingHelp);
     }
 
-    private void updateProgress(int progressSteps) {
-        if (mProgressSteps == progressSteps) {
+    private void updateProgress(int remainingSteps, int totalSteps) {
+        if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
             return;
         }
-        mProgressSteps = progressSteps;
+        mRemainingSteps = remainingSteps;
+        mTotalSteps = totalSteps;
 
-        if (mEnrollHelper == null) {
-            Log.e(TAG, "updateState: UDFPS enroll helper was null");
-            return;
+        final int progressSteps = Math.max(0, totalSteps - remainingSteps);
+
+        // If needed, add 1 to progress and total steps to account for initial touch.
+        final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps;
+        final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps;
+
+        final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal);
+
+        if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
+            mProgressAnimator.cancel();
         }
 
-        int index = 0;
-        int prevThreshold = 0;
-        while (index < mSegments.size()) {
-            final UdfpsEnrollProgressBarSegment segment = mSegments.get(index);
-            final int thresholdSteps = mEnrollHelper.getStageThresholdSteps(mTotalSteps, index);
-            if (progressSteps >= thresholdSteps && segment.getProgress() < 1f) {
-                segment.updateProgress(1f);
-                break;
-            } else if (progressSteps >= prevThreshold && progressSteps < thresholdSteps) {
-                final int relativeSteps = progressSteps - prevThreshold;
-                final int relativeThreshold = thresholdSteps - prevThreshold;
-                final float segmentProgress = (float) relativeSteps / (float) relativeThreshold;
-                segment.updateProgress(segmentProgress);
-                break;
-            }
+        mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
+        mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
+        mProgressAnimator.addUpdateListener(mProgressUpdateListener);
+        mProgressAnimator.start();
 
-            index++;
-            prevThreshold = thresholdSteps;
-        }
-
-        if (progressSteps >= mTotalSteps) {
-            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
-                segment.startCompletionAnimation();
-            }
-        } else {
-            for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
-                segment.cancelCompletionAnimation();
-            }
+        if (remainingSteps == 0) {
+            startCompletionAnimation();
+        } else if (remainingSteps > 0) {
+            rollBackCompletionAnimation();
         }
     }
 
-    private void updateFillColor(boolean isShowingHelp) {
-        if (mIsShowingHelp == isShowingHelp) {
+    private void updateFillColor(boolean showingHelp) {
+        if (mShowingHelp == showingHelp) {
             return;
         }
-        mIsShowingHelp = isShowingHelp;
+        mShowingHelp = showingHelp;
 
-        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
-            segment.updateFillColor(isShowingHelp);
+        if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
+            mFillColorAnimator.cancel();
         }
+
+        @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
+        mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
+        mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+        mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
+        mFillColorAnimator.start();
+    }
+
+    private void startCompletionAnimation() {
+        if (mComplete) {
+            return;
+        }
+        mComplete = true;
+
+        if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
+            mCheckmarkAnimator.cancel();
+        }
+
+        mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f);
+        mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS);
+        mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS);
+        mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator);
+        mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
+        mCheckmarkAnimator.start();
+    }
+
+    private void rollBackCompletionAnimation() {
+        if (!mComplete) {
+            return;
+        }
+        mComplete = false;
+
+        // Adjust duration based on how much of the completion animation has played.
+        final float animatedFraction = mCheckmarkAnimator != null
+                ? mCheckmarkAnimator.getAnimatedFraction()
+                : 0f;
+        final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction);
+
+        if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
+            mCheckmarkAnimator.cancel();
+        }
+
+        mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f);
+        mCheckmarkAnimator.setDuration(durationMs);
+        mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
+        mCheckmarkAnimator.start();
     }
 
     @Override
@@ -145,12 +235,55 @@
         // Progress starts from the top, instead of the right
         canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
 
-        // Draw each of the enroll segments.
-        for (final UdfpsEnrollProgressBarSegment segment : mSegments) {
-            segment.draw(canvas);
+        final float halfPaddingPx = mStrokeWidthPx / 2f;
+
+        if (mProgress < 1f) {
+            // Draw the background color of the progress circle.
+            canvas.drawArc(
+                    halfPaddingPx,
+                    halfPaddingPx,
+                    getBounds().right - halfPaddingPx,
+                    getBounds().bottom - halfPaddingPx,
+                    0f /* startAngle */,
+                    360f /* sweepAngle */,
+                    false /* useCenter */,
+                    mBackgroundPaint);
+        }
+
+        if (mProgress > 0f) {
+            // Draw the filled portion of the progress circle.
+            canvas.drawArc(
+                    halfPaddingPx,
+                    halfPaddingPx,
+                    getBounds().right - halfPaddingPx,
+                    getBounds().bottom - halfPaddingPx,
+                    0f /* startAngle */,
+                    360f * mProgress /* sweepAngle */,
+                    false /* useCenter */,
+                    mFillPaint);
         }
 
         canvas.restore();
+
+        if (mCheckmarkScale > 0f) {
+            final float offsetScale = (float) Math.sqrt(2) / 2f;
+            final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale;
+            final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale;
+            final float centerX = getBounds().centerX() + centerXOffset;
+            final float centerY = getBounds().centerY() + centerYOffset;
+
+            final float boundsXOffset =
+                    mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale;
+            final float boundsYOffset =
+                    mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale;
+
+            final int left = Math.round(centerX - boundsXOffset);
+            final int top = Math.round(centerY - boundsYOffset);
+            final int right = Math.round(centerX + boundsXOffset);
+            final int bottom = Math.round(centerY + boundsYOffset);
+            mCheckmarkDrawable.setBounds(left, top, right, bottom);
+            mCheckmarkDrawable.draw(canvas);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
deleted file mode 100644
index bd6ab44..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarSegment.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-import android.util.TypedValue;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.systemui.R;
-
-/**
- * A single segment of the UDFPS enrollment progress bar.
- */
-public class UdfpsEnrollProgressBarSegment {
-    private static final String TAG = "UdfpsProgressBarSegment";
-
-    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
-    private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
-    private static final long OVER_SWEEP_ANIMATION_DELAY_MS = 200L;
-    private static final long OVER_SWEEP_ANIMATION_DURATION_MS = 200L;
-
-    private static final float STROKE_WIDTH_DP = 12f;
-
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-
-    @NonNull private final Rect mBounds;
-    @NonNull private final Runnable mInvalidateRunnable;
-    private final float mStartAngle;
-    private final float mSweepAngle;
-    private final float mMaxOverSweepAngle;
-    private final float mStrokeWidthPx;
-    @ColorInt private final int mProgressColor;
-    @ColorInt private final int mHelpColor;
-
-    @NonNull private final Paint mBackgroundPaint;
-    @NonNull private final Paint mProgressPaint;
-
-    private float mProgress = 0f;
-    private float mAnimatedProgress = 0f;
-    @Nullable private ValueAnimator mProgressAnimator;
-    @NonNull private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
-
-    private boolean mIsShowingHelp = false;
-    @Nullable private ValueAnimator mFillColorAnimator;
-    @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
-
-    private float mOverSweepAngle = 0f;
-    @Nullable private ValueAnimator mOverSweepAnimator;
-    @Nullable private ValueAnimator mOverSweepReverseAnimator;
-    @NonNull private final ValueAnimator.AnimatorUpdateListener mOverSweepUpdateListener;
-    @NonNull private final Runnable mOverSweepAnimationRunnable;
-
-    public UdfpsEnrollProgressBarSegment(@NonNull Context context, @NonNull Rect bounds,
-            float startAngle, float sweepAngle, float maxOverSweepAngle,
-            @NonNull Runnable invalidateRunnable) {
-
-        mBounds = bounds;
-        mInvalidateRunnable = invalidateRunnable;
-        mStartAngle = startAngle;
-        mSweepAngle = sweepAngle;
-        mMaxOverSweepAngle = maxOverSweepAngle;
-        mStrokeWidthPx = Utils.dpToPixels(context, STROKE_WIDTH_DP);
-        mProgressColor = context.getColor(R.color.udfps_enroll_progress);
-        mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
-
-        mBackgroundPaint = new Paint();
-        mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
-        mBackgroundPaint.setColor(context.getColor(R.color.white_disabled));
-        mBackgroundPaint.setAntiAlias(true);
-        mBackgroundPaint.setStyle(Paint.Style.STROKE);
-        mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
-
-        // Background paint color + alpha
-        final int[] attrs = new int[] {android.R.attr.colorControlNormal};
-        final TypedArray ta = context.obtainStyledAttributes(attrs);
-        @ColorInt final int tintColor = ta.getColor(0, mBackgroundPaint.getColor());
-        mBackgroundPaint.setColor(tintColor);
-        ta.recycle();
-        TypedValue alpha = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, alpha, true);
-        mBackgroundPaint.setAlpha((int) (alpha.getFloat() * 255f));
-
-        // Progress should not be color extracted
-        mProgressPaint = new Paint();
-        mProgressPaint.setStrokeWidth(mStrokeWidthPx);
-        mProgressPaint.setColor(mProgressColor);
-        mProgressPaint.setAntiAlias(true);
-        mProgressPaint.setStyle(Paint.Style.STROKE);
-        mProgressPaint.setStrokeCap(Paint.Cap.ROUND);
-
-        mProgressUpdateListener = animation -> {
-            mAnimatedProgress = (float) animation.getAnimatedValue();
-            mInvalidateRunnable.run();
-        };
-
-        mFillColorUpdateListener = animation -> {
-            mProgressPaint.setColor((int) animation.getAnimatedValue());
-            mInvalidateRunnable.run();
-        };
-
-        mOverSweepUpdateListener = animation -> {
-            mOverSweepAngle = (float) animation.getAnimatedValue();
-            mInvalidateRunnable.run();
-        };
-        mOverSweepAnimationRunnable = () -> {
-            if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
-                mOverSweepAnimator.cancel();
-            }
-            mOverSweepAnimator = ValueAnimator.ofFloat(mOverSweepAngle, mMaxOverSweepAngle);
-            mOverSweepAnimator.setDuration(OVER_SWEEP_ANIMATION_DURATION_MS);
-            mOverSweepAnimator.addUpdateListener(mOverSweepUpdateListener);
-            mOverSweepAnimator.start();
-        };
-    }
-
-    /**
-     * Draws this segment to the given canvas.
-     */
-    public void draw(@NonNull Canvas canvas) {
-        final float halfPaddingPx = mStrokeWidthPx / 2f;
-
-        if (mAnimatedProgress < 1f) {
-            // Draw the unfilled background color of the segment.
-            canvas.drawArc(
-                    halfPaddingPx,
-                    halfPaddingPx,
-                    mBounds.right - halfPaddingPx,
-                    mBounds.bottom - halfPaddingPx,
-                    mStartAngle,
-                    mSweepAngle,
-                    false /* useCenter */,
-                    mBackgroundPaint);
-        }
-
-        if (mAnimatedProgress > 0f) {
-            // Draw the filled progress portion of the segment.
-            canvas.drawArc(
-                    halfPaddingPx,
-                    halfPaddingPx,
-                    mBounds.right - halfPaddingPx,
-                    mBounds.bottom - halfPaddingPx,
-                    mStartAngle,
-                    mSweepAngle * mAnimatedProgress + mOverSweepAngle,
-                    false /* useCenter */,
-                    mProgressPaint);
-        }
-    }
-
-    /**
-     * @return The fill progress of this segment, in the range [0, 1]. If fill progress is being
-     * animated, returns the value it is animating to.
-     */
-    public float getProgress() {
-        return mProgress;
-    }
-
-    /**
-     * Updates the fill progress of this segment, animating if necessary.
-     *
-     * @param progress The new fill progress, in the range [0, 1].
-     */
-    public void updateProgress(float progress) {
-        updateProgress(progress, PROGRESS_ANIMATION_DURATION_MS);
-    }
-
-    private void updateProgress(float progress, long animationDurationMs) {
-        if (mProgress == progress) {
-            return;
-        }
-        mProgress = progress;
-
-        if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
-            mProgressAnimator.cancel();
-        }
-
-        mProgressAnimator = ValueAnimator.ofFloat(mAnimatedProgress, progress);
-        mProgressAnimator.setDuration(animationDurationMs);
-        mProgressAnimator.addUpdateListener(mProgressUpdateListener);
-        mProgressAnimator.start();
-    }
-
-    /**
-     * Updates the fill color of this segment, animating if necessary.
-     *
-     * @param isShowingHelp Whether fill color should indicate that a help message is being shown.
-     */
-    public void updateFillColor(boolean isShowingHelp) {
-        if (mIsShowingHelp == isShowingHelp) {
-            return;
-        }
-        mIsShowingHelp = isShowingHelp;
-
-        if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
-            mFillColorAnimator.cancel();
-        }
-
-        @ColorInt final int targetColor = isShowingHelp ? mHelpColor : mProgressColor;
-        mFillColorAnimator = ValueAnimator.ofArgb(mProgressPaint.getColor(), targetColor);
-        mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
-        mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
-        mFillColorAnimator.start();
-    }
-
-    /**
-     * Queues and runs the completion animation for this segment.
-     */
-    public void startCompletionAnimation() {
-        final boolean hasCallback = mHandler.hasCallbacks(mOverSweepAnimationRunnable);
-        if (hasCallback || mOverSweepAngle >= mMaxOverSweepAngle) {
-            Log.d(TAG, "startCompletionAnimation skipped: hasCallback = " + hasCallback
-                    + ", mOverSweepAngle = " + mOverSweepAngle);
-            return;
-        }
-
-        // Reset sweep angle back to zero if the animation is being rolled back.
-        if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
-            mOverSweepReverseAnimator.cancel();
-            mOverSweepAngle = 0f;
-        }
-
-        // Clear help color and start filling the segment if it isn't already.
-        if (mAnimatedProgress < 1f) {
-            updateProgress(1f, OVER_SWEEP_ANIMATION_DELAY_MS);
-            updateFillColor(false /* isShowingHelp */);
-        }
-
-        // Queue the animation to run after fill completes.
-        mHandler.postDelayed(mOverSweepAnimationRunnable, OVER_SWEEP_ANIMATION_DELAY_MS);
-    }
-
-    /**
-     * Cancels (and reverses, if necessary) a queued or running completion animation.
-     */
-    public void cancelCompletionAnimation() {
-        // Cancel the animation if it's queued or running.
-        mHandler.removeCallbacks(mOverSweepAnimationRunnable);
-        if (mOverSweepAnimator != null && mOverSweepAnimator.isRunning()) {
-            mOverSweepAnimator.cancel();
-        }
-
-        // Roll back the animation if it has at least partially run.
-        if (mOverSweepAngle > 0f) {
-            if (mOverSweepReverseAnimator != null && mOverSweepReverseAnimator.isRunning()) {
-                mOverSweepReverseAnimator.cancel();
-            }
-
-            final float completion = mOverSweepAngle / mMaxOverSweepAngle;
-            final long proratedDuration = (long) (OVER_SWEEP_ANIMATION_DURATION_MS * completion);
-            mOverSweepReverseAnimator = ValueAnimator.ofFloat(mOverSweepAngle, 0f);
-            mOverSweepReverseAnimator.setDuration(proratedDuration);
-            mOverSweepReverseAnimator.addUpdateListener(mOverSweepUpdateListener);
-            mOverSweepReverseAnimator.start();
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
index 729838e..93df2cf 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollView.java
@@ -73,7 +73,6 @@
     }
 
     void setEnrollHelper(UdfpsEnrollHelper enrollHelper) {
-        mFingerprintProgressDrawable.setEnrollHelper(enrollHelper);
         mFingerprintDrawable.setEnrollHelper(enrollHelper);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
index 495366c..d1ea45c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
@@ -126,7 +126,7 @@
         mInputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN;
         mIsBouncerVisible = mKeyguardViewManager.bouncerIsOrWillBeShowing();
         mConfigurationController.addCallback(mConfigurationListener);
-        mPanelExpansionStateManager.addListener(mPanelExpansionListener);
+        mPanelExpansionStateManager.addExpansionListener(mPanelExpansionListener);
         updateAlpha();
         updatePauseAuth();
 
@@ -145,7 +145,7 @@
         mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
         mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
         mConfigurationController.removeCallback(mConfigurationListener);
-        mPanelExpansionStateManager.removeListener(mPanelExpansionListener);
+        mPanelExpansionStateManager.removeExpansionListener(mPanelExpansionListener);
         if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
             mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
index 50d2dd1..6c52b5e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
@@ -64,6 +64,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerImpl;
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
@@ -169,12 +170,19 @@
     @Provides
     static HeadsUpManagerPhone provideHeadsUpManagerPhone(
             Context context,
+            HeadsUpManagerLogger headsUpManagerLogger,
             StatusBarStateController statusBarStateController,
             KeyguardBypassController bypassController,
             GroupMembershipManager groupManager,
             ConfigurationController configurationController) {
-        return new HeadsUpManagerPhone(context, statusBarStateController, bypassController,
-                groupManager, configurationController);
+        return new HeadsUpManagerPhone(
+                context,
+                headsUpManagerLogger,
+                statusBarStateController,
+                bypassController,
+                groupManager,
+                configurationController
+        );
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 46e2274..df950d7 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -52,6 +52,14 @@
         return factory.create("NotifLog", 1000);
     }
 
+    /** Provides a logging buffer for all logs related to the data layer of notifications. */
+    @Provides
+    @SysUISingleton
+    @NotificationHeadsUpLog
+    public static LogBuffer provideNotificationHeadsUpLogBuffer(LogBufferFactory factory) {
+        return factory.create("NotifHeadsUpLog", 1000);
+    }
+
     /** Provides a logging buffer for all logs related to managing notification sections. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
new file mode 100644
index 0000000..fcc184a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotificationHeadsUpLog.java
@@ -0,0 +1,33 @@
+/*
+ * 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/** A {@link LogBuffer} for heads up notification-related messages. */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface NotificationHeadsUpLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 97bcb00..4959c7d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -222,13 +222,12 @@
      */
     public void createNavigationBars(final boolean includeDefaultDisplay,
             RegisterStatusBarResult result) {
-        if (initializeTaskbarIfNecessary()) {
-            return;
-        }
-
+        // Don't need to create nav bar on the default display if we initialize TaskBar.
+        final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
+                && !initializeTaskbarIfNecessary();
         Display[] displays = mDisplayManager.getDisplays();
         for (Display display : displays) {
-            if (includeDefaultDisplay || display.getDisplayId() != DEFAULT_DISPLAY) {
+            if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
                 createNavigationBar(display, null /* savedState */, result);
             }
         }
@@ -246,12 +245,15 @@
             return;
         }
 
-        if (mIsTablet) {
+        final int displayId = display.getDisplayId();
+        final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;
+
+        // We may show TaskBar on the default display for large screen device. Don't need to create
+        // navigation bar for this case.
+        if (mIsTablet && isOnDefaultDisplay) {
             return;
         }
 
-        final int displayId = display.getDisplayId();
-        final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;
         final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
 
         try {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 1784f73..cdf770f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -178,7 +178,10 @@
 
     @Override
     public void setSquishinessFraction(float squishinessFraction) {
-        // No-op, paged layouts are not squishy.
+        int nPages = mPages.size();
+        for (int i = 0; i < nPages; i++) {
+            mPages.get(i).setSquishinessFraction(squishinessFraction);
+        }
     }
 
     private void updateListening() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
index 48546009..c1c146d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSSquishinessController.kt
@@ -3,15 +3,14 @@
 import android.view.ViewGroup
 import com.android.systemui.qs.dagger.QSFragmentModule.QQS_FOOTER
 import com.android.systemui.qs.dagger.QSScope
-import com.android.systemui.qs.tileimpl.HeightOverrideable
 import javax.inject.Inject
 import javax.inject.Named
 
 @QSScope
 class QSSquishinessController @Inject constructor(
-    private val qsTileHost: QSTileHost,
     @Named(QQS_FOOTER) private val qqsFooterActionsView: FooterActionsView,
     private val qsAnimator: QSAnimator,
+    private val qsPanelController: QSPanelController,
     private val quickQSPanelController: QuickQSPanelController
 ) {
 
@@ -34,18 +33,10 @@
      * Change the height of all tiles and repositions their siblings.
      */
     private fun updateSquishiness() {
-        // Update tile positions in the layout
+        (qsPanelController.tileLayout as QSPanel.QSTileLayout).setSquishinessFraction(squishiness)
         val tileLayout = quickQSPanelController.tileLayout as TileLayout
         tileLayout.setSquishinessFraction(squishiness)
 
-        // Adjust their heights as well
-        for (tile in qsTileHost.tiles) {
-            val tileView = quickQSPanelController.getTileView(tile)
-            (tileView as? HeightOverrideable)?.let {
-                it.squishinessFraction = squishiness
-            }
-        }
-
         // Calculate how much we should move the footer
         val tileHeightOffset = tileLayout.height - tileLayout.tilesHeight
         val footerTopMargin = (qqsFooterActionsView.layoutParams as ViewGroup.MarginLayoutParams)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index 58c0508..7f08e5b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -13,6 +13,7 @@
 import com.android.systemui.R;
 import com.android.systemui.qs.QSPanel.QSTileLayout;
 import com.android.systemui.qs.QSPanelControllerBase.TileRecord;
+import com.android.systemui.qs.tileimpl.HeightOverrideable;
 
 import java.util.ArrayList;
 
@@ -285,5 +286,11 @@
         }
         mSquishinessFraction = squishinessFraction;
         layoutTileRecords(mRecords.size(), false /* forLayout */);
+
+        for (TileRecord record : mRecords) {
+            if (record.tileView instanceof HeightOverrideable) {
+                ((HeightOverrideable) record.tileView).setSquishinessFraction(mSquishinessFraction);
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
index f6dbb0b9..563c4cd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java
@@ -128,7 +128,9 @@
     private boolean mCanConfigMobileData;
 
     // Wi-Fi entries
+    @VisibleForTesting
     protected WifiEntry mConnectedWifiEntry;
+    @VisibleForTesting
     protected int mWifiEntriesCount;
 
     // Wi-Fi scanning progress bar
@@ -334,6 +336,9 @@
         mSeeAllLayout.setOnClickListener(v -> onClickSeeMoreButton());
         mWiFiToggle.setOnCheckedChangeListener(
                 (buttonView, isChecked) -> {
+                    if (isChecked) {
+                        mWifiScanNotifyLayout.setVisibility(View.GONE);
+                    }
                     buttonView.setChecked(isChecked);
                     mWifiManager.setWifiEnabled(isChecked);
                 });
@@ -576,12 +581,12 @@
     @WorkerThread
     public void onAccessPointsChanged(@Nullable List<WifiEntry> wifiEntries,
             @Nullable WifiEntry connectedEntry) {
-        mConnectedWifiEntry = connectedEntry;
-        mWifiEntriesCount = wifiEntries == null ? 0 : wifiEntries.size();
-        mAdapter.setWifiEntries(wifiEntries, mWifiEntriesCount);
         mHandler.post(() -> {
-            mAdapter.notifyDataSetChanged();
+            mConnectedWifiEntry = connectedEntry;
+            mWifiEntriesCount = wifiEntries == null ? 0 : wifiEntries.size();
             updateDialog(false /* shouldUpdateMobileNetwork */);
+            mAdapter.setWifiEntries(wifiEntries, mWifiEntriesCount);
+            mAdapter.notifyDataSetChanged();
         });
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 6e3d6a8..6d78b9f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -266,6 +266,7 @@
 
     private Animator mScreenshotAnimation;
     private RequestCallback mCurrentRequestCallback;
+    private String mPackageName = "";
 
     private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) {
         @Override
@@ -275,7 +276,8 @@
                     if (DEBUG_UI) {
                         Log.d(TAG, "Corner timeout hit");
                     }
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT);
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
+                            mPackageName);
                     ScreenshotController.this.dismissScreenshot(false);
                     break;
                 default:
@@ -354,12 +356,13 @@
         mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
     }
 
-    void takeScreenshotFullscreen(Consumer<Uri> finisher, RequestCallback requestCallback) {
+    void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
+            RequestCallback requestCallback) {
         mCurrentRequestCallback = requestCallback;
         DisplayMetrics displayMetrics = new DisplayMetrics();
         getDefaultDisplay().getRealMetrics(displayMetrics);
         takeScreenshotInternal(
-                finisher,
+                topComponent, finisher,
                 new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
     }
 
@@ -383,13 +386,15 @@
             screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight());
         }
         mCurrentRequestCallback = requestCallback;
-        saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, showFlash);
+        saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
+                showFlash);
     }
 
     /**
      * Displays a screenshot selector
      */
-    void takeScreenshotPartial(final Consumer<Uri> finisher, RequestCallback requestCallback) {
+    void takeScreenshotPartial(ComponentName topComponent,
+            final Consumer<Uri> finisher, RequestCallback requestCallback) {
         mScreenshotView.reset();
         mCurrentRequestCallback = requestCallback;
 
@@ -398,7 +403,7 @@
         mScreenshotView.requestApplyInsets();
 
         mScreenshotView.takePartialScreenshot(
-                rect -> takeScreenshotInternal(finisher, rect));
+                rect -> takeScreenshotInternal(topComponent, finisher, rect));
     }
 
     /**
@@ -489,7 +494,8 @@
     /**
      * Takes a screenshot of the current display and shows an animation.
      */
-    private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) {
+    private void takeScreenshotInternal(ComponentName topComponent, Consumer<Uri> finisher,
+            Rect crop) {
         mScreenshotTakenInPortrait =
                 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
 
@@ -507,7 +513,7 @@
             return;
         }
 
-        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true);
+        saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
     }
 
     private Bitmap captureScreenshot(Rect crop) {
@@ -537,7 +543,7 @@
     }
 
     private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
-            Insets screenInsets, boolean showFlash) {
+            Insets screenInsets, ComponentName topComponent, boolean showFlash) {
         if (mAccessibilityManager.isEnabled()) {
             AccessibilityEvent event =
                     new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
@@ -550,7 +556,7 @@
         if (mScreenshotView.isAttachedToWindow()) {
             // if we didn't already dismiss for another reason
             if (!mScreenshotView.isDismissing()) {
-                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED);
+                mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, mPackageName);
             }
             if (DEBUG_WINDOW) {
                 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
@@ -558,6 +564,8 @@
             }
             mScreenshotView.reset();
         }
+        mPackageName = topComponent == null ? "" : topComponent.getPackageName();
+        mScreenshotView.setPackageName(mPackageName);
 
         mScreenshotView.updateOrientation(
                 mWindowManager.getCurrentWindowMetrics().getWindowInsets());
@@ -798,11 +806,11 @@
                     }
                     finisher.accept(imageData.uri);
                     if (imageData.uri == null) {
-                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
+                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
                         mNotificationsController.notifyScreenshotError(
                                 R.string.screenshot_failed_to_save_text);
                     } else {
-                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
+                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
                         mScreenshotHandler.post(() -> Toast.makeText(mContext,
                                 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
                     }
@@ -981,11 +989,11 @@
      */
     private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
         if (imageData.uri == null) {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
             mNotificationsController.notifyScreenshotError(
                     R.string.screenshot_failed_to_save_text);
         } else {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index fbc6790..a7f8bca 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -163,6 +163,7 @@
     private SwipeDismissHandler mSwipeDismissHandler;
     private InputMonitorCompat mInputMonitor;
     private boolean mShowScrollablePreview;
+    private String mPackageName = "";
 
     private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
     private PendingInteraction mPendingInteraction;
@@ -410,6 +411,10 @@
         mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
     }
 
+    void setPackageName(String packageName) {
+        mPackageName = packageName;
+    }
+
     void updateInsets(WindowInsets insets) {
         int orientation = mContext.getResources().getConfiguration().orientation;
         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
@@ -586,7 +591,8 @@
                     if (DEBUG_INPUT) {
                         Log.d(TAG, "dismiss button clicked");
                     }
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
+                    mUiEventLogger.log(
+                            ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, 0, mPackageName);
                     animateDismissal();
                 });
                 mDismissButton.setAlpha(1);
@@ -699,24 +705,25 @@
 
     void setChipIntents(ScreenshotController.SavedImageData imageData) {
         mShareChip.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             startSharedTransition(
                     imageData.shareTransition.get());
         });
         mEditChip.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
             startSharedTransition(
                     imageData.editTransition.get());
         });
         mScreenshotPreview.setOnClickListener(v -> {
-            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
+            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
             startSharedTransition(
                     imageData.editTransition.get());
         });
         if (mQuickShareChip != null) {
             mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
                     () -> {
-                        mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
+                        mUiEventLogger.log(
+                                ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0, mPackageName);
                         animateDismissal();
                     });
         }
@@ -746,7 +753,8 @@
                 actionChip.setIcon(smartAction.getIcon(), false);
                 actionChip.setPendingIntent(smartAction.actionIntent,
                         () -> {
-                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
+                            mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED,
+                                    0, mPackageName);
                             animateDismissal();
                         });
                 actionChip.setAlpha(1);
@@ -1124,7 +1132,7 @@
                     if (DEBUG_DISMISS) {
                         Log.d(TAG, "dismiss triggered via swipe gesture");
                     }
-                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED);
+                    mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0, mPackageName);
                     animateDismissal(createSwipeDismissAnimation());
                 } else {
                     // if we've moved, but not past the threshold, start the return animation
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index daa9d09..f380911 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -186,20 +186,22 @@
         ScreenshotHelper.ScreenshotRequest screenshotRequest =
                 (ScreenshotHelper.ScreenshotRequest) msg.obj;
 
-        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()));
+        ComponentName topComponent = screenshotRequest.getTopComponent();
+        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource()), 0,
+                topComponent == null ? "" : topComponent.getPackageName());
 
         switch (msg.what) {
             case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_FULLSCREEN");
                 }
-                mScreenshot.takeScreenshotFullscreen(uriConsumer, requestCallback);
+                mScreenshot.takeScreenshotFullscreen(topComponent, uriConsumer, requestCallback);
                 break;
             case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
                 if (DEBUG_SERVICE) {
                     Log.d(TAG, "handleMessage: TAKE_SCREENSHOT_SELECTED_REGION");
                 }
-                mScreenshot.takeScreenshotPartial(uriConsumer, requestCallback);
+                mScreenshot.takeScreenshotPartial(topComponent, uriConsumer, requestCallback);
                 break;
             case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE:
                 if (DEBUG_SERVICE) {
@@ -211,7 +213,6 @@
                 Insets insets = screenshotRequest.getInsets();
                 int taskId = screenshotRequest.getTaskId();
                 int userId = screenshotRequest.getUserId();
-                ComponentName topComponent = screenshotRequest.getTopComponent();
 
                 if (screenshot == null) {
                     Log.e(TAG, "Got null bitmap from screenshot message");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
index 9fa4609..43b3fb1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java
@@ -23,12 +23,12 @@
 import android.os.SystemClock;
 import android.util.ArrayMap;
 import android.util.ArraySet;
-import android.util.Log;
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 
 import java.util.stream.Stream;
 
@@ -41,6 +41,11 @@
     private static final String TAG = "AlertNotifManager";
     protected final Clock mClock = new Clock();
     protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();
+    protected final HeadsUpManagerLogger mLogger;
+
+    public AlertingNotificationManager(HeadsUpManagerLogger logger) {
+        mLogger = logger;
+    }
 
     /**
      * This is the list of entries that have already been removed from the
@@ -61,9 +66,7 @@
      * @param entry entry to show
      */
     public void showNotification(@NonNull NotificationEntry entry) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "showNotification");
-        }
+        mLogger.logShowNotification(entry.getKey());
         addAlertEntry(entry);
         updateNotification(entry.getKey(), true /* alert */);
         entry.setInterruption();
@@ -77,9 +80,7 @@
      * @return true if notification is removed, false otherwise
      */
     public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "removeNotification");
-        }
+        mLogger.logRemoveNotification(key, releaseImmediately);
         AlertEntry alertEntry = mAlertEntries.get(key);
         if (alertEntry == null) {
             return true;
@@ -100,11 +101,8 @@
      *              removal time
      */
     public void updateNotification(@NonNull String key, boolean alert) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "updateNotification");
-        }
-
         AlertEntry alertEntry = mAlertEntries.get(key);
+        mLogger.logUpdateNotification(key, alert, alertEntry != null);
         if (alertEntry == null) {
             // the entry was released before this update (i.e by a listener) This can happen
             // with the groupmanager
@@ -121,9 +119,7 @@
      * Clears all managed notifications.
      */
     public void releaseAllImmediately() {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "releaseAllImmediately");
-        }
+        mLogger.logReleaseAllImmediately();
         // A copy is necessary here as we are changing the underlying map.  This would cause
         // undefined behavior if we iterated over the key set directly.
         ArraySet<String> keysToRemove = new ArraySet<>(mAlertEntries.keySet());
@@ -300,9 +296,7 @@
          * @param updatePostTime whether or not to refresh the post time
          */
         public void updateEntry(boolean updatePostTime) {
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "updateEntry");
-            }
+            mLogger.logUpdateEntry(updatePostTime);
 
             long currentTime = mClock.currentTimeMillis();
             mEarliestRemovaltime = currentTime + mMinimumDisplayTime;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
index 77e329f..03d8e7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LightRevealScrim.kt
@@ -208,11 +208,6 @@
     lateinit var isScrimOpaqueChangedListener: Consumer<Boolean>
 
     /**
-     * A runnable to call when the scrim has been fully revealed. This is only invoked once
-     */
-    var fullyRevealedRunnable: Runnable? = null
-
-    /**
      * How much of the underlying views are revealed, in percent. 0 means they will be completely
      * obscured and 1 means they'll be fully visible.
      */
@@ -223,20 +218,10 @@
 
                 revealEffect.setRevealAmountOnScrim(value, this)
                 updateScrimOpaque()
-                maybeTriggerFullyRevealedRunnable()
                 invalidate()
             }
         }
 
-    private fun maybeTriggerFullyRevealedRunnable() {
-        if (revealAmount == 1.0f) {
-            fullyRevealedRunnable?.let {
-                it.run()
-                fullyRevealedRunnable = null
-            }
-        }
-    }
-
     /**
      * The [LightRevealEffect] used to manipulate the radial gradient whenever [revealAmount]
      * changes.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index eb89be1..46004db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.BiometricUnlockController
+import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
@@ -73,10 +74,6 @@
         private const val TAG = "DepthController"
     }
 
-    /**
-     * Did we already unblur while dozing?
-     */
-    private var alreadyUnblurredWhileDozing = false
     lateinit var root: View
     private var blurRoot: View? = null
     private var keyguardAnimator: Animator? = null
@@ -233,11 +230,9 @@
     private val keyguardStateCallback = object : KeyguardStateController.Callback {
         override fun onKeyguardFadingAwayChanged() {
             if (!keyguardStateController.isKeyguardFadingAway ||
-                    !biometricUnlockController.isWakeAndUnlock) {
+                    biometricUnlockController.mode != MODE_WAKE_AND_UNLOCK) {
                 return
             }
-            // When wakeAndUnlocking the screen remains dozing, so we have to manually trigger
-            // the unblur earlier
 
             keyguardAnimator?.cancel()
             keyguardAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
@@ -259,7 +254,6 @@
                 })
                 start()
             }
-            alreadyUnblurredWhileDozing = statusBarStateController.dozeAmount != 0.0f
         }
 
         override fun onKeyguardShowingChanged() {
@@ -281,24 +275,10 @@
             if (isDozing) {
                 shadeAnimation.finishIfRunning()
                 brightnessMirrorSpring.finishIfRunning()
-
-                // unset this for safety, to be ready for the next wakeup
-                alreadyUnblurredWhileDozing = false
             }
         }
 
         override fun onDozeAmountChanged(linear: Float, eased: Float) {
-            if (alreadyUnblurredWhileDozing) {
-                if (linear == 0.0f) {
-                    // We finished waking up, let's reset
-                    alreadyUnblurredWhileDozing = false
-                } else {
-                    // We've already handled the unbluring from the keyguardAnimator above.
-                    // if we would continue, we'd play another unzoom / blur animation from the
-                    // dozing changing.
-                    return
-                }
-            }
             wakeAndUnlockBlurRadius = blurUtils.blurRadiusOfRatio(eased)
             scheduleUpdate()
         }
@@ -458,7 +438,6 @@
             it.println("blursDisabledForAppLaunch: $blursDisabledForAppLaunch")
             it.println("qsPanelExpansion: $qsPanelExpansion")
             it.println("transitionToFullShadeProgress: $transitionToFullShadeProgress")
-            it.println("alreadyUnblurredWhileDozing: $alreadyUnblurredWhileDozing")
             it.println("lastAppliedBlur: $lastAppliedBlur")
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
index ed195af..2098a1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallListener;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.EncryptionHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -101,6 +102,7 @@
     private final StatusBarLocationPublisher mLocationPublisher;
     private final FeatureFlags mFeatureFlags;
     private final NotificationIconAreaController mNotificationIconAreaController;
+    private final PanelExpansionStateManager mPanelExpansionStateManager;
     private final StatusBarIconController mStatusBarIconController;
 
     private List<String> mBlockedIcons = new ArrayList<>();
@@ -126,6 +128,7 @@
             SystemStatusAnimationScheduler animationScheduler,
             StatusBarLocationPublisher locationPublisher,
             NotificationIconAreaController notificationIconAreaController,
+            PanelExpansionStateManager panelExpansionStateManager,
             FeatureFlags featureFlags,
             StatusBarIconController statusBarIconController,
             KeyguardStateController keyguardStateController,
@@ -140,6 +143,7 @@
         mAnimationScheduler = animationScheduler;
         mLocationPublisher = locationPublisher;
         mNotificationIconAreaController = notificationIconAreaController;
+        mPanelExpansionStateManager = panelExpansionStateManager;
         mFeatureFlags = featureFlags;
         mStatusBarIconController = statusBarIconController;
         mKeyguardStateController = keyguardStateController;
@@ -363,7 +367,7 @@
 
     private boolean shouldHideNotificationIcons() {
         final Optional<StatusBar> statusBarOptional = mStatusBarOptionalLazy.get();
-        if (!mStatusBar.isClosed()
+        if (!mPanelExpansionStateManager.isClosed()
                 && statusBarOptional.map(
                         StatusBar::hideStatusBarIconsWhenExpanded).orElse(false)) {
             return true;
@@ -409,7 +413,7 @@
      * don't set the clock GONE otherwise it'll mess up the animation.
      */
     private int clockHiddenMode() {
-        if (!mStatusBar.isClosed() && !mKeyguardStateController.isShowing()
+        if (!mPanelExpansionStateManager.isClosed() && !mKeyguardStateController.isShowing()
                 && !mStatusBarStateController.isDozing()) {
             return View.INVISIBLE;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index c81196d..01acce3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -38,6 +38,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 
 import java.io.FileDescriptor;
@@ -101,11 +102,12 @@
     //  Constructor:
 
     public HeadsUpManagerPhone(@NonNull final Context context,
+            HeadsUpManagerLogger logger,
             StatusBarStateController statusBarStateController,
             KeyguardBypassController bypassController,
             GroupMembershipManager groupMembershipManager,
             ConfigurationController configurationController) {
-        super(context);
+        super(context, logger);
         Resources resources = mContext.getResources();
         mExtensionTime = resources.getInteger(R.integer.ambient_notification_extension_time);
         mAutoHeadsUpNotificationDecay = resources.getInteger(
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 f47bfd6..b05a9bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -35,9 +35,9 @@
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL;
-import static com.android.systemui.statusbar.phone.PanelBar.STATE_CLOSED;
-import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPEN;
-import static com.android.systemui.statusbar.phone.PanelBar.STATE_OPENING;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
 
 import static java.lang.Float.isNaN;
 
@@ -183,6 +183,7 @@
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger.LockscreenUiEvent;
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
+import com.android.systemui.statusbar.phone.panelstate.PanelState;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -859,6 +860,8 @@
                 new DynamicPrivacyControlListener();
         dynamicPrivacyController.addListener(dynamicPrivacyControlListener);
 
+        panelExpansionStateManager.addStateListener(this::onPanelStateChanged);
+
         mBottomAreaShadeAlphaAnimator = ValueAnimator.ofFloat(1f, 0);
         mBottomAreaShadeAlphaAnimator.addUpdateListener(animation -> {
             mBottomAreaShadeAlpha = (float) animation.getAnimatedValue();
@@ -1696,7 +1699,7 @@
             // it's possible that nothing animated, so we replicate the termination
             // conditions of panelExpansionChanged here
             // TODO(b/200063118): This can likely go away in a future refactor CL.
-            mBar.updateState(STATE_CLOSED);
+            getPanelExpansionStateManager().updateState(STATE_CLOSED);
         }
     }
 
@@ -1781,7 +1784,7 @@
 
     @Override
     public void fling(float vel, boolean expand) {
-        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
+        GestureRecorder gr = mStatusBar.getGestureRecorder();
         if (gr != null) {
             gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
         }
@@ -2373,8 +2376,10 @@
     private void updateQsExpansion() {
         if (mQs == null) return;
         float qsExpansionFraction = computeQsExpansionFraction();
+        float squishiness = mNotificationStackScrollLayoutController
+                .getNotificationSquishinessFraction();
         mQs.setQsExpansion(qsExpansionFraction, getExpandedFraction(), getHeaderTranslation(),
-                mNotificationStackScrollLayoutController.getNotificationSquishinessFraction());
+                mQsExpandImmediate || mQsExpanded ? 1f : squishiness);
         mSplitShadeHeaderController.setQsExpandedFraction(qsExpansionFraction);
         mMediaHierarchyManager.setQsExpansion(qsExpansionFraction);
         int qsPanelBottomY = calculateQsBottomPosition(qsExpansionFraction);
@@ -4851,37 +4856,27 @@
         mView.removeCallbacks(mMaybeHideExpandedRunnable);
     }
 
-    private final PanelBar.PanelStateChangeListener mPanelStateChangeListener =
-            new PanelBar.PanelStateChangeListener() {
+    @PanelState
+    private int mCurrentPanelState = STATE_CLOSED;
 
-                @PanelBar.PanelState
-                private int mCurrentState = STATE_CLOSED;
+    private void onPanelStateChanged(@PanelState int state) {
+        mAmbientState.setIsShadeOpening(state == STATE_OPENING);
+        updateQSExpansionEnabledAmbient();
 
-                @Override
-                public void onStateChanged(@PanelBar.PanelState int state) {
-                    mAmbientState.setIsShadeOpening(state == STATE_OPENING);
-                    updateQSExpansionEnabledAmbient();
-
-                    if (state == STATE_OPEN && mCurrentState != state) {
-                        mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
-                    }
-                    if (state == STATE_OPENING) {
-                        mStatusBar.makeExpandedVisible(false);
-                    }
-                    if (state == STATE_CLOSED) {
-                        // Close the status bar in the next frame so we can show the end of the
-                        // animation.
-                        mView.post(mMaybeHideExpandedRunnable);
-                    }
-                    mCurrentState = state;
-                }
-            };
-
-    public PanelBar.PanelStateChangeListener getPanelStateChangeListener() {
-        return mPanelStateChangeListener;
+        if (state == STATE_OPEN && mCurrentPanelState != state) {
+            mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        }
+        if (state == STATE_OPENING) {
+            mStatusBar.makeExpandedVisible(false);
+        }
+        if (state == STATE_CLOSED) {
+            // Close the status bar in the next frame so we can show the end of the
+            // animation.
+            mView.post(mMaybeHideExpandedRunnable);
+        }
+        mCurrentPanelState = state;
     }
 
-
     /** Returns the handler that the status bar should forward touches to. */
     public PhoneStatusBarView.TouchEventHandler getStatusBarTouchEventHandler() {
         return getTouchHandler()::onTouchForwardedFromStatusBar;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 0b3e040..01587f7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -447,7 +447,7 @@
         setDragDownHelper(mLockscreenShadeTransitionController.getTouchHelper());
 
         mDepthController.setRoot(mView);
-        mPanelExpansionStateManager.addListener(mDepthController);
+        mPanelExpansionStateManager.addExpansionListener(mDepthController);
     }
 
     public NotificationShadeWindowView getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
deleted file mode 100644
index e90258d..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelBar.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2012 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.phone;
-
-import static java.lang.Float.isNaN;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.annotation.IntDef;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import java.lang.annotation.Retention;
-
-public abstract class PanelBar extends FrameLayout {
-    public static final boolean DEBUG = false;
-    public static final String TAG = PanelBar.class.getSimpleName();
-    private static final boolean SPEW = false;
-    private static final String PANEL_BAR_SUPER_PARCELABLE = "panel_bar_super_parcelable";
-    private static final String STATE = "state";
-    protected float mPanelFraction;
-
-    public static final void LOG(String fmt, Object... args) {
-        if (!DEBUG) return;
-        Log.v(TAG, String.format(fmt, args));
-    }
-
-    /** Enum for the current state of the panel. */
-    @Retention(SOURCE)
-    @IntDef({STATE_CLOSED, STATE_OPENING, STATE_OPEN})
-    @interface PanelState {}
-    public static final int STATE_CLOSED = 0;
-    public static final int STATE_OPENING = 1;
-    public static final int STATE_OPEN = 2;
-
-    @Nullable private PanelStateChangeListener mPanelStateChangeListener;
-    private int mState = STATE_CLOSED;
-    private boolean mTracking;
-
-    /** Updates the panel state if necessary. */
-    public void updateState(@PanelState int state) {
-        if (DEBUG) LOG("update state: %d -> %d", mState, state);
-        if (mState != state) {
-            go(state);
-        }
-    }
-
-    private void go(@PanelState int state) {
-        if (DEBUG) LOG("go state: %d -> %d", mState, state);
-        mState = state;
-        if (mPanelStateChangeListener != null) {
-            mPanelStateChangeListener.onStateChanged(state);
-        }
-    }
-
-    @Override
-    protected Parcelable onSaveInstanceState() {
-        Bundle bundle = new Bundle();
-        bundle.putParcelable(PANEL_BAR_SUPER_PARCELABLE, super.onSaveInstanceState());
-        bundle.putInt(STATE, mState);
-        return bundle;
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Parcelable state) {
-        if (state == null || !(state instanceof Bundle)) {
-            super.onRestoreInstanceState(state);
-            return;
-        }
-
-        Bundle bundle = (Bundle) state;
-        super.onRestoreInstanceState(bundle.getParcelable(PANEL_BAR_SUPER_PARCELABLE));
-        if (((Bundle) state).containsKey(STATE)) {
-            go(bundle.getInt(STATE, STATE_CLOSED));
-        }
-    }
-
-    public PanelBar(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-    }
-
-    /** Sets the listener that will be notified of panel state changes. */
-    public void setPanelStateChangeListener(PanelStateChangeListener listener) {
-        mPanelStateChangeListener = listener;
-    }
-
-    /**
-     * @param frac the fraction from the expansion in [0, 1]
-     * @param expanded whether the panel is currently expanded; this is independent from the
-     *                 fraction as the panel also might be expanded if the fraction is 0
-     */
-    public void panelExpansionChanged(float frac, boolean expanded) {
-        if (isNaN(frac)) {
-            throw new IllegalArgumentException("frac cannot be NaN");
-        }
-        boolean fullyClosed = true;
-        boolean fullyOpened = false;
-        if (SPEW) LOG("panelExpansionChanged: start state=%d, f=%.1f", mState, frac);
-        mPanelFraction = frac;
-        // adjust any other panels that may be partially visible
-        if (expanded) {
-            if (mState == STATE_CLOSED) {
-                go(STATE_OPENING);
-            }
-            fullyClosed = false;
-            fullyOpened = frac >= 1f;
-        }
-        if (fullyOpened && !mTracking) {
-            go(STATE_OPEN);
-        } else if (fullyClosed && !mTracking && mState != STATE_CLOSED) {
-            go(STATE_CLOSED);
-        }
-
-        if (SPEW) LOG("panelExpansionChanged: end state=%d [%s%s ]", mState,
-                fullyOpened?" fullyOpened":"", fullyClosed?" fullyClosed":"");
-    }
-
-    public boolean isClosed() {
-        return mState == STATE_CLOSED;
-    }
-
-    public void onTrackingStarted() {
-        mTracking = true;
-    }
-
-    public void onTrackingStopped(boolean expand) {
-        mTracking = false;
-    }
-
-    /** An interface that will be notified of panel state changes. */
-    public interface PanelStateChangeListener {
-        /** Called when the state changes. */
-        void onStateChanged(@PanelState int state);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
index 481401b..249f988 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java
@@ -23,7 +23,7 @@
 import android.widget.FrameLayout;
 
 public abstract class PanelView extends FrameLayout {
-    public static final boolean DEBUG = PanelBar.DEBUG;
+    public static final boolean DEBUG = false;
     public static final String TAG = PanelView.class.getSimpleName();
     private PanelViewController.TouchHandler mTouchHandler;
 
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 b508ddf..38cf787 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelViewController.java
@@ -67,7 +67,7 @@
 import java.io.PrintWriter;
 
 public abstract class PanelViewController {
-    public static final boolean DEBUG = PanelBar.DEBUG;
+    public static final boolean DEBUG = PanelView.DEBUG;
     public static final String TAG = PanelView.class.getSimpleName();
     private static final int NO_FIXED_DURATION = -1;
     private static final long SHADE_OPEN_SPRING_OUT_DURATION = 350L;
@@ -153,8 +153,6 @@
     private boolean mAnimateAfterExpanding;
     private boolean mIsFlinging;
 
-    PanelBar mBar;
-
     private String mViewName;
     private float mInitialTouchY;
     private float mInitialTouchX;
@@ -462,7 +460,6 @@
 
     protected void onTrackingStopped(boolean expand) {
         mTracking = false;
-        mBar.onTrackingStopped(expand);
         mStatusBar.onTrackingStopped(expand);
         updatePanelExpansionAndVisibility();
     }
@@ -470,7 +467,6 @@
     protected void onTrackingStarted() {
         endClosing();
         mTracking = true;
-        mBar.onTrackingStarted();
         mStatusBar.onTrackingStarted();
         notifyExpandingStarted();
         updatePanelExpansionAndVisibility();
@@ -848,10 +844,6 @@
         return mTracking;
     }
 
-    public void setBar(PanelBar panelBar) {
-        mBar = panelBar;
-    }
-
     public void collapse(boolean delayed, float speedUpFactor) {
         if (DEBUG) logf("collapse: " + this);
         if (canPanelBeCollapsed()) {
@@ -1089,12 +1081,9 @@
      *   {@link #updateVisibility()}? That would allow us to make this method private.
      */
     public void updatePanelExpansionAndVisibility() {
-        if (mBar != null) {
-            mBar.panelExpansionChanged(mExpandedFraction, isExpanded());
-        }
-        updateVisibility();
         mPanelExpansionStateManager.onPanelExpansionChanged(
                 mExpandedFraction, isExpanded(), mTracking);
+        updateVisibility();
     }
 
     public boolean isExpanded() {
@@ -1453,4 +1442,8 @@
     protected float getExpansionFraction() {
         return mExpandedFraction;
     }
+
+    protected PanelExpansionStateManager getPanelExpansionStateManager() {
+        return mPanelExpansionStateManager;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 5305ecd..0acf2ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -30,6 +30,7 @@
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
 import com.android.internal.policy.SystemBarUtils;
@@ -42,10 +43,8 @@
 
 import java.util.Objects;
 
-public class PhoneStatusBarView extends PanelBar {
+public class PhoneStatusBarView extends FrameLayout {
     private static final String TAG = "PhoneStatusBarView";
-    private static final boolean DEBUG = StatusBar.DEBUG;
-    private static final boolean DEBUG_GESTURES = false;
     private final StatusBarContentInsetsProvider mContentInsetsProvider;
 
     StatusBar mBar;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index cef0613..1077347 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -271,7 +271,7 @@
                 ScrimController.this.onThemeChanged();
             }
         });
-        panelExpansionStateManager.addListener(
+        panelExpansionStateManager.addExpansionListener(
                 (fraction, expanded, tracking) -> setRawPanelExpansionFraction(fraction)
         );
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 315ef77..c0849d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -339,7 +339,6 @@
     }
 
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    private boolean mCallingFadingAwayAfterReveal;
     private StatusBarCommandQueueCallbacks mCommandQueueCallbacks;
 
     void setWindowState(int state) {
@@ -902,7 +901,7 @@
         mStartingSurfaceOptional = startingSurfaceOptional;
         lockscreenShadeTransitionController.setStatusbar(this);
 
-        mPanelExpansionStateManager.addListener(this::onPanelExpansionChanged);
+        mPanelExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged);
 
         mBubbleExpandListener =
                 (isExpanding, key) -> mContext.getMainExecutor().execute(() -> {
@@ -1149,7 +1148,7 @@
         mNotificationLogger.setUpWithContainer(notifListContainer);
 
         mNotificationIconAreaController.setupShelf(mNotificationShelfController);
-        mPanelExpansionStateManager.addListener(mWakeUpCoordinator);
+        mPanelExpansionStateManager.addExpansionListener(mWakeUpCoordinator);
 
         mUserSwitcherController.init(mNotificationShadeWindowView);
 
@@ -1164,12 +1163,8 @@
                     PhoneStatusBarView oldStatusBarView = mStatusBarView;
                     mStatusBarView = (PhoneStatusBarView) statusBarFragment.getView();
                     mStatusBarView.setBar(this);
-                    mStatusBarView.setPanelStateChangeListener(
-                            mNotificationPanelViewController.getPanelStateChangeListener());
                     mStatusBarView.setScrimController(mScrimController);
 
-                    mNotificationPanelViewController.setBar(mStatusBarView);
-
                     mPhoneStatusBarViewController = mPhoneStatusBarViewControllerFactory
                             .create(mStatusBarView, mNotificationPanelViewController
                                     .getStatusBarTouchEventHandler());
@@ -1221,6 +1216,7 @@
                                 mAnimationScheduler,
                                 mStatusBarLocationPublisher,
                                 mNotificationIconAreaController,
+                                mPanelExpansionStateManager,
                                 mFeatureFlags,
                                 mStatusBarIconController,
                                 mKeyguardStateController,
@@ -3121,20 +3117,8 @@
     public void fadeKeyguardWhilePulsing() {
         mNotificationPanelViewController.fadeOut(0, FADE_KEYGUARD_DURATION_PULSING,
                 ()-> {
-                Runnable finishFading = () -> {
-                    mCallingFadingAwayAfterReveal = false;
-                    hideKeyguard();
-                    mStatusBarKeyguardViewManager.onKeyguardFadedAway();
-                };
-                if (mLightRevealScrim.getRevealAmount() != 1.0f) {
-                    mCallingFadingAwayAfterReveal = true;
-                    // We're still revealing the Light reveal, let's only go to keyguard once
-                    // that has finished and nothing moves anymore.
-                    // Going there introduces lots of jank
-                    mLightRevealScrim.setFullyRevealedRunnable(finishFading);
-                } else {
-                    finishFading.run();
-                }
+                hideKeyguard();
+                mStatusBarKeyguardViewManager.onKeyguardFadedAway();
             }).start();
     }
 
@@ -4281,7 +4265,7 @@
                         + "mStatusBarKeyguardViewManager was null");
                 return;
             }
-            if (mKeyguardStateController.isKeyguardFadingAway() && !mCallingFadingAwayAfterReveal) {
+            if (mKeyguardStateController.isKeyguardFadingAway()) {
                 mStatusBarKeyguardViewManager.onKeyguardFadedAway();
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 30e668a..58d2881 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -279,7 +279,7 @@
         mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
         mNotificationPanelViewController = notificationPanelViewController;
         if (panelExpansionStateManager != null) {
-            panelExpansionStateManager.addListener(this);
+            panelExpansionStateManager.addExpansionListener(this);
         }
         mBypassController = bypassController;
         mNotificationContainer = notificationContainer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
index aa748b0..2c7c8e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManager.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone.panelstate
 
+import android.annotation.IntDef
+import android.util.Log
 import androidx.annotation.FloatRange
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
@@ -23,42 +25,136 @@
 /**
  * A class responsible for managing the notification panel's current state.
  *
- * TODO(b/200063118): Move [PanelBar.panelExpansionChanged] logic to this class and make this class
- *   the one source of truth for the state of panel expansion.
+ * TODO(b/200063118): Make this class the one source of truth for the state of panel expansion.
  */
 @SysUISingleton
 class PanelExpansionStateManager @Inject constructor() {
 
-    private val listeners: MutableList<PanelExpansionListener> = mutableListOf()
+    private val expansionListeners = mutableListOf<PanelExpansionListener>()
+    private val stateListeners = mutableListOf<PanelStateListener>()
 
+    @PanelState private var state: Int = STATE_CLOSED
     @FloatRange(from = 0.0, to = 1.0) private var fraction: Float = 0f
     private var expanded: Boolean = false
     private var tracking: Boolean = false
 
     /**
-     * Adds a listener that will be notified when the panel expansion has changed.
+     * Adds a listener that will be notified when the panel expansion fraction has changed.
      *
      * Listener will also be immediately notified with the current values.
      */
-    fun addListener(listener: PanelExpansionListener) {
-        listeners.add(listener)
+    fun addExpansionListener(listener: PanelExpansionListener) {
+        expansionListeners.add(listener)
         listener.onPanelExpansionChanged(fraction, expanded, tracking)
     }
 
-    /** Removes a listener. */
-    fun removeListener(listener: PanelExpansionListener) {
-        listeners.remove(listener)
+    /** Removes an expansion listener. */
+    fun removeExpansionListener(listener: PanelExpansionListener) {
+        expansionListeners.remove(listener)
     }
 
-    /** Called when the panel expansion has changed. Notifies all listeners of change. */
+    /** Adds a listener that will be notified when the panel state has changed. */
+    fun addStateListener(listener: PanelStateListener) {
+        stateListeners.add(listener)
+    }
+
+    /** Removes a state listener. */
+    fun removeStateListener(listener: PanelStateListener) {
+        stateListeners.remove(listener)
+    }
+
+    /** Returns true if the panel is currently closed and false otherwise. */
+    fun isClosed(): Boolean = state == STATE_CLOSED
+
+    /**
+     * Called when the panel expansion has changed.
+     *
+     * @param fraction the fraction from the expansion in [0, 1]
+     * @param expanded whether the panel is currently expanded; this is independent from the
+     * fraction as the panel also might be expanded if the fraction is 0.
+     * @param tracking whether we're currently tracking the user's gesture.
+     */
     fun onPanelExpansionChanged(
         @FloatRange(from = 0.0, to = 1.0) fraction: Float,
         expanded: Boolean,
         tracking: Boolean
     ) {
+        require(!fraction.isNaN()) { "fraction cannot be NaN" }
+        val oldState = state
+
         this.fraction = fraction
         this.expanded = expanded
         this.tracking = tracking
-        listeners.forEach { it.onPanelExpansionChanged(fraction, expanded, tracking) }
+
+        var fullyClosed = true
+        var fullyOpened = false
+
+        if (expanded) {
+            if (this.state == STATE_CLOSED) {
+                updateStateInternal(STATE_OPENING)
+            }
+            fullyClosed = false
+            fullyOpened = fraction >= 1f
+        }
+
+        if (fullyOpened && !tracking) {
+            updateStateInternal(STATE_OPEN)
+        } else if (fullyClosed && !tracking && this.state != STATE_CLOSED) {
+            updateStateInternal(STATE_CLOSED)
+        }
+
+        debugLog(
+            "panelExpansionChanged:" +
+                    "start state=${oldState.stateToString()} " +
+                    "end state=${state.stateToString()} " +
+                    "f=$fraction " +
+                    "expanded=$expanded " +
+                    "tracking=$tracking" +
+                    "${if (fullyOpened) " fullyOpened" else ""} " +
+                    if (fullyClosed) " fullyClosed" else ""
+        )
+
+        expansionListeners.forEach { it.onPanelExpansionChanged(fraction, expanded, tracking) }
+    }
+
+    /** Updates the panel state if necessary.  */
+    fun updateState(@PanelState state: Int) {
+        debugLog("update state: ${this.state.stateToString()} -> ${state.stateToString()}")
+        if (this.state != state) {
+            updateStateInternal(state)
+        }
+    }
+
+    private fun updateStateInternal(@PanelState state: Int) {
+        debugLog("go state: ${this.state.stateToString()} -> ${state.stateToString()}")
+        this.state = state
+        stateListeners.forEach { it.onPanelStateChanged(state) }
+    }
+
+    private fun debugLog(msg: String) {
+        if (!DEBUG) return
+        Log.v(TAG, msg)
     }
 }
+
+/** Enum for the current state of the panel.  */
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(value = [STATE_CLOSED, STATE_OPENING, STATE_OPEN])
+internal annotation class PanelState
+
+const val STATE_CLOSED = 0
+const val STATE_OPENING = 1
+const val STATE_OPEN = 2
+
+@PanelState
+private fun Int.stateToString(): String {
+    return when (this) {
+        STATE_CLOSED -> "CLOSED"
+        STATE_OPENING -> "OPENING"
+        STATE_OPEN -> "OPEN"
+        else -> this.toString()
+    }
+}
+
+private const val DEBUG = false
+private val TAG = PanelExpansionStateManager::class.simpleName
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
new file mode 100644
index 0000000..e299592
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/panelstate/PanelStateListener.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.panelstate
+
+/** A listener interface to be notified of state change events for the notification panel. */
+interface PanelStateListener {
+    /** Called when the panel's expansion state has changed.   */
+    fun onPanelStateChanged(@PanelState state: Int)
+}
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 e0b0dd36..9587261 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java
@@ -26,7 +26,6 @@
 import android.database.ContentObserver;
 import android.provider.Settings;
 import android.util.ArrayMap;
-import android.util.Log;
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.logging.MetricsLogger;
@@ -81,7 +80,8 @@
         }
     }
 
-    public HeadsUpManager(@NonNull final Context context) {
+    public HeadsUpManager(@NonNull final Context context, HeadsUpManagerLogger logger) {
+        super(logger);
         mContext = context;
         mAccessibilityMgr = Dependency.get(AccessibilityManagerWrapper.class);
         mUiEventLogger = Dependency.get(UiEventLogger.class);
@@ -102,9 +102,7 @@
                         context.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1);
                 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) {
                     mSnoozeLengthMs = packageSnoozeLengthMs;
-                    if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                        Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs);
-                    }
+                    mLogger.logSnoozeLengthChange(packageSnoozeLengthMs);
                 }
             }
         };
@@ -145,9 +143,7 @@
 
     protected void setEntryPinned(
             @NonNull HeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "setEntryPinned: " + isPinned);
-        }
+        mLogger.logSetEntryPinned(headsUpEntry.mEntry.getKey(), isPinned);
         NotificationEntry entry = headsUpEntry.mEntry;
         if (entry.isRowPinned() != isPinned) {
             entry.setRowPinned(isPinned);
@@ -198,10 +194,7 @@
         if (hasPinnedNotification == mHasPinnedNotification) {
             return;
         }
-        if (Log.isLoggable(TAG, Log.VERBOSE)) {
-            Log.v(TAG, "Pinned mode changed: " + mHasPinnedNotification + " -> " +
-                       hasPinnedNotification);
-        }
+        mLogger.logUpdatePinnedMode(hasPinnedNotification);
         mHasPinnedNotification = hasPinnedNotification;
         if (mHasPinnedNotification) {
             MetricsLogger.count(mContext, "note_peek", 1);
@@ -219,12 +212,11 @@
         Long snoozedUntil = mSnoozedPackages.get(key);
         if (snoozedUntil != null) {
             if (snoozedUntil > mClock.currentTimeMillis()) {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                    Log.v(TAG, key + " snoozed");
-                }
+                mLogger.logIsSnoozedReturned(key);
                 return true;
             }
-            mSnoozedPackages.remove(packageName);
+            mLogger.logPackageUnsnoozed(key);
+            mSnoozedPackages.remove(key);
         }
         return false;
     }
@@ -236,8 +228,9 @@
         for (String key : mAlertEntries.keySet()) {
             AlertEntry entry = getHeadsUpEntry(key);
             String packageName = entry.mEntry.getSbn().getPackageName();
-            mSnoozedPackages.put(snoozeKey(packageName, mUser),
-                    mClock.currentTimeMillis() + mSnoozeLengthMs);
+            String snoozeKey = snoozeKey(packageName, mUser);
+            mLogger.logPackageSnoozed(snoozeKey);
+            mSnoozedPackages.put(snoozeKey, mClock.currentTimeMillis() + mSnoozeLengthMs);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
new file mode 100644
index 0000000..2bdf62bb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.policy
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.dagger.NotificationHeadsUpLog
+import javax.inject.Inject
+
+/** Logger for [HeadsUpManager]. */
+class HeadsUpManagerLogger @Inject constructor(
+    @NotificationHeadsUpLog private val buffer: LogBuffer
+) {
+    fun logPackageSnoozed(snoozeKey: String) {
+        buffer.log(TAG, INFO, {
+            str1 = snoozeKey
+        }, {
+            "package snoozed $str1"
+        })
+    }
+
+    fun logPackageUnsnoozed(snoozeKey: String) {
+        buffer.log(TAG, INFO, {
+            str1 = snoozeKey
+        }, {
+            "package unsnoozed $str1"
+        })
+    }
+
+    fun logIsSnoozedReturned(snoozeKey: String) {
+        buffer.log(TAG, INFO, {
+            str1 = snoozeKey
+        }, {
+            "package snoozed when queried $str1"
+        })
+    }
+
+    fun logReleaseAllImmediately() {
+        buffer.log(TAG, INFO, { }, {
+            "release all immediately"
+        })
+    }
+
+    fun logShowNotification(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+        }, {
+            "show notification $str1"
+        })
+    }
+
+    fun logRemoveNotification(key: String, releaseImmediately: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            bool1 = releaseImmediately
+        }, {
+            "remove notification $str1 releaseImmediately: $bool1"
+        })
+    }
+
+    fun logUpdateNotification(key: String, alert: Boolean, hasEntry: Boolean) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+            bool1 = alert
+            bool2 = hasEntry
+        }, {
+            "update notification $str1 alert: $bool1 hasEntry: $bool2"
+        })
+    }
+
+    fun logUpdateEntry(updatePostTime: Boolean) {
+        buffer.log(TAG, INFO, {
+            bool1 = updatePostTime
+        }, {
+            "update entry updatePostTime: $bool1"
+        })
+    }
+
+    fun logSnoozeLengthChange(packageSnoozeLengthMs: Int) {
+        buffer.log(TAG, INFO, {
+            int1 = packageSnoozeLengthMs
+        }, {
+            "snooze length changed: ${int1}ms"
+        })
+    }
+
+    fun logSetEntryPinned(key: String, isPinned: Boolean) {
+        buffer.log(TAG, VERBOSE, {
+            str1 = key
+            bool1 = isPinned
+        }, {
+            "set entry pinned $str1 pinned: $bool1"
+        })
+    }
+
+    fun logUpdatePinnedMode(hasPinnedNotification: Boolean) {
+        buffer.log(TAG, INFO, {
+            bool1 = hasPinnedNotification
+        }, {
+            "has pinned notification changed to $bool1"
+        })
+    }
+}
+
+private const val TAG = "HeadsUpManager"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 923aff1..65518d6 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -65,6 +65,7 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedControllerImpl;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyControllerImpl;
 import com.android.systemui.statusbar.policy.SensorPrivacyController;
@@ -162,12 +163,19 @@
     @Provides
     static HeadsUpManagerPhone provideHeadsUpManagerPhone(
             Context context,
+            HeadsUpManagerLogger headsUpManagerLogger,
             StatusBarStateController statusBarStateController,
             KeyguardBypassController bypassController,
             NotificationGroupManagerLegacy groupManager,
             ConfigurationController configurationController) {
-        return new HeadsUpManagerPhone(context, statusBarStateController, bypassController,
-                groupManager, configurationController);
+        return new HeadsUpManagerPhone(
+                context,
+                headsUpManagerLogger,
+                statusBarStateController,
+                bypassController,
+                groupManager,
+                configurationController
+        );
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index e570598..5aa3617 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -377,7 +377,8 @@
     private void playTouchFeedback() {
         if (System.currentTimeMillis() - mLastToggledRingerOn < TOUCH_FEEDBACK_TIMEOUT_MS) {
             try {
-                mAudioService.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD);
+                mAudioService.playSoundEffect(AudioManager.FX_KEYPRESS_STANDARD,
+                        UserHandle.USER_CURRENT);
             } catch (RemoteException e) {
                 // ignore
             }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
index c98a504..2d51092 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
@@ -18,8 +18,12 @@
 
 import android.animation.Animator
 import android.graphics.Insets
+import android.app.ActivityManager
+import android.app.ActivityTaskManager
+import android.content.ComponentName
 import android.graphics.Rect
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
+import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
 import android.hardware.biometrics.SensorProperties
 import android.hardware.display.DisplayManager
@@ -60,9 +64,11 @@
 import org.mockito.Mockito.any
 import org.mockito.Mockito.anyFloat
 import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 
@@ -84,6 +90,8 @@
     @Mock
     lateinit var windowManager: WindowManager
     @Mock
+    lateinit var activityTaskManager: ActivityTaskManager
+    @Mock
     lateinit var sidefpsView: View
     @Mock
     lateinit var displayManager: DisplayManager
@@ -144,7 +152,8 @@
 
         sideFpsController = SidefpsController(
             context.createDisplayContext(display), layoutInflater, fingerprintManager,
-            windowManager, overviewProxyService, displayManager, executor, handler
+            windowManager, activityTaskManager, overviewProxyService, displayManager, executor,
+            handler
         )
 
         overlayController = ArgumentCaptor.forClass(ISidefpsController::class.java).apply {
@@ -211,12 +220,23 @@
         testIgnoredFor(REASON_AUTH_KEYGUARD)
     }
 
-    private fun testIgnoredFor(reason: Int) {
-        overlayController.show(SENSOR_ID, reason)
+    @Test
+    fun testShowsForMostSettings() = testWithDisplay {
+        `when`(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpEnrollTask()))
+        testIgnoredFor(REASON_AUTH_SETTINGS, ignored = false)
+    }
 
+    @Test
+    fun testIgnoredForVerySpecificSettings() = testWithDisplay {
+        `when`(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpSettingsTask()))
+        testIgnoredFor(REASON_AUTH_SETTINGS)
+    }
+
+    private fun testIgnoredFor(reason: Int, ignored: Boolean = true) {
+        overlayController.show(SENSOR_ID, reason)
         executor.runAllReady()
 
-        verify(windowManager, never()).addView(any(), any())
+        verify(windowManager, if (ignored) never() else times(1)).addView(any(), any())
     }
 
     @Test
@@ -267,4 +287,9 @@
 private fun insetsForLargeNavbar() = insetsWithBottom(100)
 private fun insetsWithBottom(bottom: Int) = WindowInsets.Builder()
     .setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, bottom))
-    .build()
\ No newline at end of file
+    .build()
+private fun fpEnrollTask() = settingsTask(".biometrics.fingerprint.FingerprintEnrollEnrolling")
+private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
+private fun settingsTask(cls: String) = ActivityManager.RunningTaskInfo().apply {
+    topActivity = ComponentName.createRelative("com.android.settings", cls)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index cfac9cb..d90eb73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -405,15 +405,75 @@
     }
 
     @Test
-    public void showUdfpsOverlay_addsViewToWindow() throws RemoteException {
-        mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
-                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+    public void showUdfpsOverlay_addsViewToWindow_bp() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_BP);
+    }
+
+    @Test
+    public void showUdfpsOverlay_addsViewToWindow_keyguard() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_KEYGUARD);
+    }
+
+    @Test
+    public void showUdfpsOverlay_addsViewToWindow_settings() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_SETTINGS);
+    }
+
+    @Test
+    public void showUdfpsOverlay_addsViewToWindow_enroll_locate() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR);
+    }
+
+    @Test
+    public void showUdfpsOverlay_addsViewToWindow_enroll() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_ENROLL_ENROLLING);
+    }
+
+    @Test
+    public void showUdfpsOverlay_addsViewToWindow_other() throws RemoteException {
+        showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_OTHER);
+    }
+
+    private void showUdfpsOverlay_addsViewToWindow(
+            @BiometricOverlayConstants.ShowReason int reason) throws RemoteException {
+        mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, reason,
+                mUdfpsOverlayControllerCallback);
         mFgExecutor.runAllReady();
         verify(mWindowManager).addView(eq(mUdfpsView), any());
     }
 
     @Test
-    public void hideUdfpsOverlay_removesViewFromWindow() throws RemoteException {
+    public void hideUdfpsOverlay_removesViewFromWindow_bp() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_BP);
+    }
+
+    @Test
+    public void hideUdfpsOverlay_removesViewFromWindow_keyguard() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_KEYGUARD);
+    }
+
+    @Test
+    public void hideUdfpsOverlay_removesViewFromWindow_settings() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_SETTINGS);
+    }
+
+    @Test
+    public void hideUdfpsOverlay_removesViewFromWindow_enroll_locate() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR);
+    }
+
+    @Test
+    public void hideUdfpsOverlay_removesViewFromWindow_enroll() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_ENROLL_ENROLLING);
+    }
+
+    @Test
+    public void hideUdfpsOverlay_removesViewFromWindow_other() throws RemoteException {
+        hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_OTHER);
+    }
+
+    private void hideUdfpsOverlay_removesViewFromWindow(
+            @BiometricOverlayConstants.ShowReason int reason) throws RemoteException {
         mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID,
                 BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
         mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID);
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 6f0456e..0e86964 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -171,7 +171,7 @@
 
         verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
         for (PanelExpansionListener listener : mExpansionListeners) {
-            verify(mPanelExpansionStateManager).removeListener(listener);
+            verify(mPanelExpansionStateManager).removeExpansionListener(listener);
         }
         verify(mKeyguardStateController).removeCallback(mKeyguardStateControllerCallback);
     }
@@ -435,7 +435,7 @@
 
     private void captureExpansionListeners() {
         verify(mPanelExpansionStateManager, times(2))
-                .addListener(mExpansionListenerCaptor.capture());
+                .addExpansionListener(mExpansionListenerCaptor.capture());
         // first (index=0) is from super class, UdfpsAnimationViewController.
         // second (index=1) is from UdfpsKeyguardViewController
         mExpansionListeners = mExpansionListenerCaptor.getAllValues();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
index 3059aa1..f41d7b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSSquishinessControllerTest.kt
@@ -4,13 +4,10 @@
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.qs.QSTile
-import com.android.systemui.qs.tileimpl.QSTileViewImpl
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
 import org.mockito.Mock
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
@@ -21,14 +18,13 @@
 @SmallTest
 class QSSquishinessControllerTest : SysuiTestCase() {
 
-    @Mock private lateinit var qsTileHost: QSTileHost
     @Mock private lateinit var qqsFooterActionsView: FooterActionsView
     @Mock private lateinit var qqsFooterActionsViewLP: ViewGroup.MarginLayoutParams
     @Mock private lateinit var qsAnimator: QSAnimator
+    @Mock private lateinit var qsPanelController: QSPanelController
     @Mock private lateinit var quickQsPanelController: QuickQSPanelController
-    @Mock private lateinit var qstileView: QSTileViewImpl
-    @Mock private lateinit var qstile: QSTile
     @Mock private lateinit var tileLayout: TileLayout
+    @Mock private lateinit var pagedTileLayout: PagedTileLayout
 
     @JvmField @Rule val mockitoRule = MockitoJUnit.rule()
 
@@ -36,11 +32,10 @@
 
     @Before
     fun setup() {
-        qsSquishinessController = QSSquishinessController(qsTileHost, qqsFooterActionsView,
-                qsAnimator, quickQsPanelController)
-        `when`(qsTileHost.tiles).thenReturn(mutableListOf(qstile))
-        `when`(quickQsPanelController.getTileView(any())).thenReturn(qstileView)
+        qsSquishinessController = QSSquishinessController(qqsFooterActionsView, qsAnimator,
+                qsPanelController, quickQsPanelController)
         `when`(quickQsPanelController.tileLayout).thenReturn(tileLayout)
+        `when`(qsPanelController.tileLayout).thenReturn(pagedTileLayout)
         `when`(qqsFooterActionsView.layoutParams).thenReturn(qqsFooterActionsViewLP)
     }
 
@@ -56,7 +51,7 @@
     @Test
     fun setSquishiness_updatesTiles() {
         qsSquishinessController.squishiness = 0.5f
-        verify(qstileView).squishinessFraction = 0.5f
         verify(tileLayout).setSquishinessFraction(0.5f)
+        verify(pagedTileLayout).setSquishinessFraction(0.5f)
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
index 5e1fea5..b6e8979 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java
@@ -99,7 +99,8 @@
                 mInternetDialogController, true, true, true, mock(UiEventLogger.class), mHandler,
                 mBgExecutor);
         mInternetDialog.mAdapter = mInternetAdapter;
-        mInternetDialog.onAccessPointsChanged(mWifiEntries, mInternetWifiEntry);
+        mInternetDialog.mConnectedWifiEntry = mInternetWifiEntry;
+        mInternetDialog.mWifiEntriesCount = mWifiEntries.size();
         mInternetDialog.show();
 
         mDialogView = mInternetDialog.mDialogView;
@@ -209,7 +210,7 @@
     @Test
     public void updateDialog_wifiOnAndNoConnectedWifi_hideConnectedWifi() {
         // The precondition WiFi ON is already in setUp()
-        mInternetDialog.onAccessPointsChanged(mWifiEntries, null /* connectedEntry*/);
+        mInternetDialog.mConnectedWifiEntry = null;
         doReturn(false).when(mInternetDialogController).activeNetworkIsCellular();
 
         mInternetDialog.updateDialog(false);
@@ -220,7 +221,7 @@
     @Test
     public void updateDialog_wifiOnAndNoWifiList_hideWifiListAndSeeAll() {
         // The precondition WiFi ON is already in setUp()
-        mInternetDialog.onAccessPointsChanged(null /* wifiEntries */, mInternetWifiEntry);
+        mInternetDialog.mWifiEntriesCount = 0;
 
         mInternetDialog.updateDialog(false);
 
@@ -366,7 +367,8 @@
     public void showProgressBar_wifiEnabledWithoutWifiEntries_showProgressBarThenHideSearch() {
         Mockito.reset(mHandler);
         when(mWifiManager.isWifiEnabled()).thenReturn(true);
-        mInternetDialog.onAccessPointsChanged(null /* wifiEntries */, null /* connectedEntry*/);
+        mInternetDialog.mConnectedWifiEntry = null;
+        mInternetDialog.mWifiEntriesCount = 0;
 
         mInternetDialog.showProgressBar();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
index dee6020..9bf8775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/AlertingNotificationManagerTest.java
@@ -23,6 +23,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 
@@ -42,6 +43,7 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -81,6 +83,7 @@
         private AlertEntry mLastCreatedEntry;
 
         private TestableAlertingNotificationManager() {
+            super(mock(HeadsUpManagerLogger.class));
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
             mHandler = mTestHandler;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index c5d1e3a..e3dcfab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
 import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
 import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
@@ -129,9 +130,14 @@
                 Optional.of((mock(Bubbles.class))),
                 mock(DumpManager.class));
         mGroupExpansionManager = mGroupMembershipManager;
-        mHeadsUpManager = new HeadsUpManagerPhone(mContext, mStatusBarStateController,
-                mock(KeyguardBypassController.class), mock(NotificationGroupManagerLegacy.class),
-                mock(ConfigurationControllerImpl.class));
+        mHeadsUpManager = new HeadsUpManagerPhone(
+                mContext,
+                mock(HeadsUpManagerLogger.class),
+                mStatusBarStateController,
+                mock(KeyguardBypassController.class),
+                mock(NotificationGroupManagerLegacy.class),
+                mock(ConfigurationControllerImpl.class)
+        );
         mGroupMembershipManager.setHeadsUpManager(mHeadsUpManager);
         mIconManager = new IconManager(
                 mock(CommonNotifCollection.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
index be54a6a..e4c4c63 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragmentTest.java
@@ -48,6 +48,7 @@
 import com.android.systemui.statusbar.connectivity.NetworkController;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
@@ -261,6 +262,7 @@
                 mAnimationScheduler,
                 mLocationPublisher,
                 mMockNotificationAreaController,
+                new PanelExpansionStateManager(),
                 mock(FeatureFlags.class),
                 mStatusBarIconController,
                 mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index 83b6d2c..0f419c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -39,6 +39,7 @@
 import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -57,6 +58,7 @@
 
     private HeadsUpManagerPhone mHeadsUpManager;
 
+    @Mock private HeadsUpManagerLogger mHeadsUpManagerLogger;
     @Mock private NotificationGroupManagerLegacy mGroupManager;
     @Mock private View mNotificationShadeWindowView;
     @Mock private VisualStabilityManager mVSManager;
@@ -69,14 +71,21 @@
     private final class TestableHeadsUpManagerPhone extends HeadsUpManagerPhone {
         TestableHeadsUpManagerPhone(
                 Context context,
+                HeadsUpManagerLogger headsUpManagerLogger,
                 NotificationGroupManagerLegacy groupManager,
                 VisualStabilityManager vsManager,
                 StatusBarStateController statusBarStateController,
                 KeyguardBypassController keyguardBypassController,
                 ConfigurationController configurationController
         ) {
-            super(context, statusBarStateController, keyguardBypassController,
-                    groupManager, configurationController);
+            super(
+                    context,
+                    headsUpManagerLogger,
+                    statusBarStateController,
+                    keyguardBypassController,
+                    groupManager,
+                    configurationController
+            );
             setup(vsManager);
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
@@ -96,8 +105,15 @@
         when(mVSManager.isReorderingAllowed()).thenReturn(true);
         mDependency.injectMockDependency(NotificationShadeWindowController.class);
         mDependency.injectMockDependency(ConfigurationController.class);
-        mHeadsUpManager = new TestableHeadsUpManagerPhone(mContext, mGroupManager, mVSManager,
-                mStatusBarStateController, mBypassController, mConfigurationController);
+        mHeadsUpManager = new TestableHeadsUpManagerPhone(
+                mContext,
+                mHeadsUpManagerLogger,
+                mGroupManager,
+                mVSManager,
+                mStatusBarStateController,
+                mBypassController,
+                mConfigurationController
+        );
         super.setUp();
         mHeadsUpManager.mHandler = mTestHandler;
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
index 80d9c08..b717d28 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationGroupAlertTransferHelperTest.java
@@ -48,6 +48,7 @@
 import com.android.systemui.statusbar.notification.row.RowContentBindParams;
 import com.android.systemui.statusbar.notification.row.RowContentBindStage;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.HeadsUpManagerLogger;
 import com.android.wm.shell.bubbles.Bubbles;
 
 import org.junit.Before;
@@ -86,7 +87,7 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mHeadsUpManager = new HeadsUpManager(mContext) {};
+        mHeadsUpManager = new HeadsUpManager(mContext, mock(HeadsUpManagerLogger.class)) {};
 
         when(mNotificationEntryManager.getPendingNotificationsIterator())
                 .thenReturn(mPendingEntries.values());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
index 99399c2..5773e91 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java
@@ -187,8 +187,6 @@
     @Mock
     private HeadsUpTouchHelper.Callback mHeadsUpCallback;
     @Mock
-    private PanelBar mPanelBar;
-    @Mock
     private KeyguardUpdateMonitor mUpdateMonitor;
     @Mock
     private KeyguardBypassController mKeyguardBypassController;
@@ -520,7 +518,6 @@
                 () -> {},
                 mNotificationShelfController);
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
-        mNotificationPanelViewController.setBar(mPanelBar);
         mNotificationPanelViewController.setKeyguardIndicationController(
                 mKeyguardIndicationController);
         ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index 300860c..e8ad5fd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -54,40 +54,6 @@
     }
 
     @Test
-    fun panelStateChanged_toStateOpening_listenerNotified() {
-        val listener = TestStateChangedListener()
-        view.setPanelStateChangeListener(listener)
-
-        view.panelExpansionChanged(0.5f, true)
-
-        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPENING)
-    }
-
-    @Test
-    fun panelStateChanged_toStateOpen_listenerNotified() {
-        val listener = TestStateChangedListener()
-        view.setPanelStateChangeListener(listener)
-
-        view.panelExpansionChanged(1f, true)
-
-        assertThat(listener.state).isEqualTo(PanelBar.STATE_OPEN)
-    }
-
-    @Test
-    fun panelStateChanged_toStateClosed_listenerNotified() {
-        val listener = TestStateChangedListener()
-        view.setPanelStateChangeListener(listener)
-
-        // First, open the panel
-        view.panelExpansionChanged(1f, true)
-
-        // Then, close it again
-        view.panelExpansionChanged(0f, false)
-
-        assertThat(listener.state).isEqualTo(PanelBar.STATE_CLOSED)
-    }
-
-    @Test
     fun onTouchEvent_listenerNotified() {
         val handler = TestTouchEventHandler()
         view.setTouchEventHandler(handler)
@@ -126,13 +92,6 @@
         // No assert needed, just testing no crash
     }
 
-    private class TestStateChangedListener : PanelBar.PanelStateChangeListener {
-        var state: Int = 0
-        override fun onStateChanged(state: Int) {
-            this.state = state
-        }
-    }
-
     private class TestTouchEventHandler : PhoneStatusBarView.TouchEventHandler {
         var lastEvent: MotionEvent? = null
         var returnValue: Boolean = false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
index e09cde9..32bad5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
@@ -33,9 +33,9 @@
     }
 
     @Test
-    fun onPanelExpansionChanged_listenersNotified() {
+    fun onPanelExpansionChanged_listenerNotified() {
         val listener = TestPanelExpansionListener()
-        panelExpansionStateManager.addListener(listener)
+        panelExpansionStateManager.addExpansionListener(listener)
         val fraction = 0.6f
         val expanded = true
         val tracking = true
@@ -48,20 +48,143 @@
     }
 
     @Test
-    fun addPanelExpansionListener_listenerNotifiedOfCurrentValues() {
+    fun addExpansionListener_listenerNotifiedOfCurrentValues() {
         val fraction = 0.6f
         val expanded = true
         val tracking = true
         panelExpansionStateManager.onPanelExpansionChanged(fraction, expanded, tracking)
         val listener = TestPanelExpansionListener()
 
-        panelExpansionStateManager.addListener(listener)
+        panelExpansionStateManager.addExpansionListener(listener)
 
         assertThat(listener.fraction).isEqualTo(fraction)
         assertThat(listener.expanded).isEqualTo(expanded)
         assertThat(listener.tracking).isEqualTo(tracking)
     }
 
+    @Test
+    fun updateState_listenerNotified() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+
+        panelExpansionStateManager.updateState(STATE_OPEN)
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* ***** [PanelExpansionStateManager.onPanelExpansionChanged] test cases *******/
+
+    /* Fraction < 1 test cases */
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedTrue_trackingFalse_becomesStateOpening() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f, expanded = true, tracking = false
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedTrue_trackingTrue_becomesStateOpening() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f, expanded = true, tracking = true
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedFalse_trackingFalse_becomesStateClosed() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        panelExpansionStateManager.updateState(STATE_OPEN)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f, expanded = false, tracking = false
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_CLOSED)
+    }
+
+    @Test
+    fun onPEC_fractionLessThanOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        panelExpansionStateManager.updateState(STATE_OPEN)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 0.5f, expanded = false, tracking = true
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* Fraction = 1 test cases */
+
+    @Test
+    fun onPEC_fractionOne_expandedTrue_trackingFalse_becomesStateOpeningThenStateOpen() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f, expanded = true, tracking = false
+        )
+
+        assertThat(listener.previousState).isEqualTo(STATE_OPENING)
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedTrue_trackingTrue_becomesStateOpening() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f, expanded = true, tracking = true
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPENING)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedFalse_trackingFalse_becomesStateClosed() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        panelExpansionStateManager.updateState(STATE_OPEN)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f, expanded = false, tracking = false
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_CLOSED)
+    }
+
+    @Test
+    fun onPEC_fractionOne_expandedFalse_trackingTrue_doesNotBecomeStateClosed() {
+        val listener = TestPanelStateListener()
+        panelExpansionStateManager.addStateListener(listener)
+        // Start out on a different state
+        panelExpansionStateManager.updateState(STATE_OPEN)
+
+        panelExpansionStateManager.onPanelExpansionChanged(
+            fraction = 1f, expanded = false, tracking = true
+        )
+
+        assertThat(listener.state).isEqualTo(STATE_OPEN)
+    }
+
+    /* ***** end [PanelExpansionStateManager.onPanelExpansionChanged] test cases ******/
+
     class TestPanelExpansionListener : PanelExpansionListener {
         var fraction: Float = 0f
         var expanded: Boolean = false
@@ -77,4 +200,14 @@
             this.tracking = tracking
         }
     }
+
+    class TestPanelStateListener : PanelStateListener {
+        @PanelState var previousState: Int = STATE_CLOSED
+        @PanelState var state: Int = STATE_CLOSED
+
+        override fun onPanelStateChanged(state: Int) {
+            this.previousState = this.state
+            this.state = state
+        }
+    }
 }
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 b53cbf7..5e852e3 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
@@ -60,7 +60,7 @@
 
     private final class TestableHeadsUpManager extends HeadsUpManager {
         TestableHeadsUpManager(Context context) {
-            super(context);
+            super(context, mock(HeadsUpManagerLogger.class));
             mMinimumDisplayTime = TEST_MINIMUM_DISPLAY_TIME;
             mAutoDismissNotificationDecay = TEST_AUTO_DISMISS_TIME;
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c22c90e..b1a1e0d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4932,6 +4932,8 @@
 
     @Override
     public void bootAnimationComplete() {
+        if (DEBUG_ALL) Slog.d(TAG, "bootAnimationComplete: Callers=" + Debug.getCallers(4));
+
         final boolean callFinishBooting;
         synchronized (this) {
             callFinishBooting = mCallFinishBooting;
@@ -7765,7 +7767,7 @@
 
         // On Automotive, at this point the system user has already been started and unlocked,
         // and some of the tasks we do here have already been done. So skip those in that case.
-        // TODO(b/132262830): this workdound shouldn't be necessary once we move the
+        // TODO(b/132262830, b/203885241): this workdound shouldn't be necessary once we move the
         // headless-user start logic to UserManager-land
         final boolean bootingSystemUser = currentUserId == UserHandle.USER_SYSTEM;
 
diff --git a/services/core/java/com/android/server/am/UserState.java b/services/core/java/com/android/server/am/UserState.java
index 1fe7608..40fc306 100644
--- a/services/core/java/com/android/server/am/UserState.java
+++ b/services/core/java/com/android/server/am/UserState.java
@@ -145,4 +145,11 @@
         proto.write(UserStateProto.SWITCHING, switching);
         proto.end(token);
     }
+
+    @Override
+    public String toString() {
+        return "[UserState: id=" + mHandle.getIdentifier() + ", state=" + stateToString(state)
+            + ", lastState=" + stateToString(lastState) + ", switching=" + switching
+            + ", tokenProvided=" + tokenProvided + "]";
+    }
 }
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationManagerInternal.java b/services/core/java/com/android/server/apphibernation/AppHibernationManagerInternal.java
index b0335fe..a3c9612 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationManagerInternal.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationManagerInternal.java
@@ -43,4 +43,9 @@
      * @see AppHibernationService#setHibernatingGlobally
      */
     public abstract void setHibernatingGlobally(String packageName, boolean isHibernating);
+
+    /**
+     * @see AppHibernationService#isOatArtifactDeletionEnabled
+     */
+    public abstract boolean isOatArtifactDeletionEnabled();
 }
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
index bd066ff..4d025c9 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
@@ -200,6 +200,14 @@
     }
 
     /**
+     * Whether global hibernation should delete ART ahead-of-time compilation artifacts and prevent
+     * package manager from re-optimizing the APK.
+     */
+    private boolean isOatArtifactDeletionEnabled() {
+        return mOatArtifactDeletionEnabled;
+    }
+
+    /**
      * Whether a package is hibernating for a given user.
      *
      * @param packageName the package to check
@@ -730,6 +738,11 @@
         public boolean isHibernatingGlobally(String packageName) {
             return mService.isHibernatingGlobally(packageName);
         }
+
+        @Override
+        public boolean isOatArtifactDeletionEnabled() {
+            return mService.isOatArtifactDeletionEnabled();
+        }
     }
 
     private final AppHibernationServiceStub mServiceStub = new AppHibernationServiceStub(this);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 2cd63b5..34191fa 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -5037,9 +5037,19 @@
         }
     }
 
-    /** @see AudioManager#playSoundEffect(int) */
-    public void playSoundEffect(int effectType) {
-        playSoundEffectVolume(effectType, -1.0f);
+    /** @see AudioManager#playSoundEffect(int, int) */
+    public void playSoundEffect(int effectType, int userId) {
+        if (querySoundEffectsEnabled(userId)) {
+            playSoundEffectVolume(effectType, -1.0f);
+        }
+    }
+
+    /**
+     * Settings has an in memory cache, so this is fast.
+     */
+    private boolean querySoundEffectsEnabled(int user) {
+        return Settings.System.getIntForUser(getContentResolver(),
+                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
     }
 
     /** @see AudioManager#playSoundEffect(int, float) */
@@ -7656,7 +7666,7 @@
                     break;
 
                 case MSG_INIT_HEADTRACKING_SENSORS:
-                    mSpatializerHelper.onInitSensors(/*init*/ msg.arg1 == 1);
+                    mSpatializerHelper.onInitSensors();
                     break;
 
                 case MSG_CHECK_MUSIC_ACTIVE:
@@ -8590,14 +8600,13 @@
 
     /**
      * post a message to schedule init/release of head tracking sensors
-     * @param init initialization if true, release if false
+     * whether to initialize or release sensors is based on the state of spatializer
      */
-    void postInitSpatializerHeadTrackingSensors(boolean init) {
+    void postInitSpatializerHeadTrackingSensors() {
         sendMsg(mAudioHandler,
                 MSG_INIT_HEADTRACKING_SENSORS,
                 SENDMSG_REPLACE,
-                /*arg1*/ init ? 1 : 0,
-                0, TAG, /*delay*/ 0);
+                /*arg1*/ 0, /*arg2*/ 0, TAG, /*delay*/ 0);
     }
 
     //==========================================================================================
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 7cd027c..6a26bea 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -63,6 +63,14 @@
     private @Nullable SensorManager mSensorManager;
 
     //------------------------------------------------------------
+    /** head tracker sensor name */
+    // TODO: replace with generic head tracker sensor name.
+    //       the current implementation refers to the "google" namespace but will be replaced
+    //       by an android name at the next API level revision, it is not Google-specific.
+    //       Also see "TODO-HT" in onInitSensors() method
+    private static final String HEADTRACKER_SENSOR =
+            "com.google.hardware.sensor.hid_dynamic.headtracker";
+
     // Spatializer state machine
     private static final int STATE_UNINITIALIZED = 0;
     private static final int STATE_NOT_SUPPORTED = 1;
@@ -81,7 +89,7 @@
     private @Nullable ISpatializer mSpat;
     private @Nullable SpatializerCallback mSpatCallback;
     private @Nullable SpatializerHeadTrackingCallback mSpatHeadTrackingCallback;
-
+    private @Nullable HelperDynamicSensorCallback mDynSensorCallback;
 
     // default attributes and format that determine basic availability of spatialization
     private static final AudioAttributes DEFAULT_ATTRIBUTES = new AudioAttributes.Builder()
@@ -209,11 +217,7 @@
             // TODO use reported spat level to change state
 
             // init sensors
-            if (level == SpatializationLevel.NONE) {
-                initSensors(/*init*/false);
-            } else {
-                postInitSensors(true);
-            }
+            postInitSensors();
         }
 
         public void onOutputChanged(int output) {
@@ -229,6 +233,7 @@
         }
     };
 
+    //------------------------------------------------------
     // spatializer head tracking callback from native
     private final class SpatializerHeadTrackingCallback
             extends ISpatializerHeadTrackingCallback.Stub {
@@ -269,6 +274,20 @@
     };
 
     //------------------------------------------------------
+    // dynamic sensor callback
+    private final class HelperDynamicSensorCallback extends SensorManager.DynamicSensorCallback {
+        @Override
+        public void onDynamicSensorConnected(Sensor sensor) {
+            postInitSensors();
+        }
+
+        @Override
+        public void onDynamicSensorDisconnected(Sensor sensor) {
+            postInitSensors();
+        }
+    }
+
+    //------------------------------------------------------
     // compatible devices
     /**
      * @return a shallow copy of the list of compatible audio devices
@@ -851,48 +870,12 @@
 
     //------------------------------------------------------
     // sensors
-    private void initSensors(boolean init) {
-        if (mSensorManager == null) {
-            mSensorManager = (SensorManager)
-                    mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
-        }
-        final int headHandle;
-        final int screenHandle;
-        if (init) {
-            if (mSensorManager == null) {
-                Log.e(TAG, "Null SensorManager, can't init sensors");
-                return;
-            }
-            // TODO replace with dynamic association of sensor for headtracker
-            Sensor headSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR);
-            headHandle = headSensor.getHandle();
-            //Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
-            //screenHandle = deviceSensor.getHandle();
-            screenHandle = -1;
-        } else {
-            // -1 is disable value
-            screenHandle = -1;
-            headHandle = -1;
-        }
-        try {
-            Log.i(TAG, "setScreenSensor:" + screenHandle);
-            mSpat.setScreenSensor(screenHandle);
-        } catch (Exception e) {
-            Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
-        }
-        try {
-            Log.i(TAG, "setHeadSensor:" + headHandle);
-            mSpat.setHeadSensor(headHandle);
-        } catch (Exception e) {
-            Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
-        }
+    private void postInitSensors() {
+        mAudioService.postInitSpatializerHeadTrackingSensors();
     }
 
-    private void postInitSensors(boolean init) {
-        mAudioService.postInitSpatializerHeadTrackingSensors(init);
-    }
-
-    synchronized void onInitSensors(boolean init) {
+    synchronized void onInitSensors() {
+        final boolean init = (mSpatLevel != SpatializationLevel.NONE);
         final String action = init ? "initializing" : "releasing";
         if (mSpat == null) {
             Log.e(TAG, "not " + action + " sensors, null spatializer");
@@ -907,7 +890,53 @@
             Log.e(TAG, "not " + action + " sensors, error querying headtracking", e);
             return;
         }
-        initSensors(init);
+        int headHandle = -1;
+        int screenHandle = -1;
+        if (init) {
+            if (mSensorManager == null) {
+                try {
+                    mSensorManager = (SensorManager)
+                            mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
+                    mDynSensorCallback = new HelperDynamicSensorCallback();
+                    mSensorManager.registerDynamicSensorCallback(mDynSensorCallback);
+                } catch (Exception e) {
+                    Log.e(TAG, "Error with SensorManager, can't initialize sensors", e);
+                    mSensorManager = null;
+                    mDynSensorCallback = null;
+                    return;
+                }
+            }
+            // initialize sensor handles
+            // TODO-HT update to non-private sensor once head tracker sensor is defined
+            for (Sensor sensor : mSensorManager.getDynamicSensorList(
+                    Sensor.TYPE_DEVICE_PRIVATE_BASE)) {
+                if (sensor.getStringType().equals(HEADTRACKER_SENSOR)) {
+                    headHandle = sensor.getHandle();
+                    break;
+                }
+            }
+            Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
+            screenHandle = screenSensor.getHandle();
+        } else {
+            if (mSensorManager != null && mDynSensorCallback != null) {
+                mSensorManager.unregisterDynamicSensorCallback(mDynSensorCallback);
+                mSensorManager = null;
+                mDynSensorCallback = null;
+            }
+            // -1 is disable value for both screen and head tracker handles
+        }
+        try {
+            Log.i(TAG, "setScreenSensor:" + screenHandle);
+            mSpat.setScreenSensor(screenHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
+        }
+        try {
+            Log.i(TAG, "setHeadSensor:" + headHandle);
+            mSpat.setHeadSensor(headHandle);
+        } catch (Exception e) {
+            Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
+        }
     }
 
     //------------------------------------------------------
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
index 031f6ee..61b8ded 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java
@@ -168,6 +168,10 @@
         return Utils.isKeyguard(getContext(), getOwnerString());
     }
 
+    private boolean isSettings() {
+        return Utils.isSettings(getContext(), getOwnerString());
+    }
+
     @Override
     protected boolean isCryptoOperation() {
         return mOperationId != 0;
@@ -499,6 +503,8 @@
     protected int getShowOverlayReason() {
         if (isKeyguard()) {
             return BiometricOverlayConstants.REASON_AUTH_KEYGUARD;
+        } else if (isSettings()) {
+            return BiometricOverlayConstants.REASON_AUTH_SETTINGS;
         } else if (isBiometricPrompt()) {
             return BiometricOverlayConstants.REASON_AUTH_BP;
         } else {
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStartUserClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStartUserClient.java
index c364dbb..2b5f495 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStartUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceStartUserClient.java
@@ -22,6 +22,7 @@
 import android.hardware.biometrics.face.IFace;
 import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.ISessionCallback;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -52,6 +53,7 @@
         try {
             final ISession newSession = getFreshDaemon().createSession(getSensorId(),
                     getTargetUserId(), mSessionCallback);
+            Binder.allowBlocking(newSession.asBinder());
             mUserStartedCallback.onUserStarted(getTargetUserId(), newSession);
             getCallback().onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStartUserClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStartUserClient.java
index 2d40c91..ee81620 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStartUserClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintStartUserClient.java
@@ -22,6 +22,7 @@
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.ISessionCallback;
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -53,6 +54,7 @@
         try {
             final ISession newSession = getFreshDaemon().createSession(getSensorId(),
                     getTargetUserId(), mSessionCallback);
+            Binder.allowBlocking(newSession.asBinder());
             mUserStartedCallback.onUserStarted(getTargetUserId(), newSession);
             getCallback().onClientFinished(this, true /* success */);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/communal/CommunalManagerService.java b/services/core/java/com/android/server/communal/CommunalManagerService.java
index d77e3b5..2fda4a5 100644
--- a/services/core/java/com/android/server/communal/CommunalManagerService.java
+++ b/services/core/java/com/android/server/communal/CommunalManagerService.java
@@ -17,27 +17,39 @@
 package com.android.server.communal;
 
 import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY;
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 
 import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_ORDERED_ID;
 
 import android.Manifest;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.TestApi;
 import android.app.KeyguardManager;
 import android.app.PendingIntent;
 import android.app.communal.ICommunalManager;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.Disabled;
+import android.compat.annotation.Overridable;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.IIntentSender;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.IntentSender;
 import android.content.pm.ActivityInfo;
-import android.database.ContentObserver;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.service.dreams.DreamManagerInternal;
 import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.LaunchAfterAuthenticationActivity;
@@ -47,6 +59,7 @@
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -55,21 +68,47 @@
  * System service for handling Communal Mode state.
  */
 public final class CommunalManagerService extends SystemService {
+    private static final String TAG = CommunalManagerService.class.getSimpleName();
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     private static final String DELIMITER = ",";
     private final Context mContext;
     private final ActivityTaskManagerInternal mAtmInternal;
     private final KeyguardManager mKeyguardManager;
     private final AtomicBoolean mCommunalViewIsShowing = new AtomicBoolean(false);
-    private final Handler mHandler = new Handler(Looper.getMainLooper());
-    private final Set<String> mEnabledApps = new HashSet<>();
-    private final SettingsObserver mSettingsObserver;
+    private final BinderService mBinderService;
+    private final PackageReceiver mPackageReceiver;
+    private final PackageManager mPackageManager;
+    private final DreamManagerInternal mDreamManagerInternal;
+
+    /**
+     * This change id is used to annotate packages which are allowed to run in communal mode.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    @TestApi
+    public static final long ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT = 200324021L;
+
+    /**
+     * This change id is used to annotate packages which can run in communal mode by default,
+     * without requiring user opt-in.
+     *
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    @TestApi
+    public static final long ALLOW_COMMUNAL_MODE_BY_DEFAULT = 203673428L;
 
     private final ActivityInterceptorCallback mActivityInterceptorCallback =
             new ActivityInterceptorCallback() {
                 @Nullable
                 @Override
                 public Intent intercept(ActivityInterceptorInfo info) {
-                    if (isActivityAllowed(info.aInfo)) {
+                    if (!shouldIntercept(info.aInfo)) {
                         return null;
                     }
 
@@ -96,63 +135,108 @@
     public CommunalManagerService(Context context) {
         super(context);
         mContext = context;
-        mSettingsObserver = new SettingsObserver();
+        mPackageManager = mContext.getPackageManager();
         mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
+        mDreamManagerInternal = LocalServices.getService(DreamManagerInternal.class);
         mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+        mBinderService = new BinderService();
+        mPackageReceiver = new PackageReceiver(mContext);
+    }
+
+    @VisibleForTesting
+    BinderService getBinderServiceInstance() {
+        return mBinderService;
     }
 
     @Override
     public void onStart() {
-        publishBinderService(Context.COMMUNAL_MANAGER_SERVICE, new BinderService());
-        mAtmInternal.registerActivityStartInterceptor(COMMUNAL_MODE_ORDERED_ID,
-                mActivityInterceptorCallback);
-
-
-        updateSelectedApps();
-        mContext.getContentResolver().registerContentObserver(Settings.Secure.getUriFor(
-                Settings.Secure.COMMUNAL_MODE_PACKAGES), false, mSettingsObserver,
-                UserHandle.USER_SYSTEM);
+        publishBinderService(Context.COMMUNAL_MANAGER_SERVICE, mBinderService);
     }
 
-    @VisibleForTesting
-    void updateSelectedApps() {
+    @Override
+    public void onBootPhase(int phase) {
+        if (phase != SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) return;
+        mAtmInternal.registerActivityStartInterceptor(
+                COMMUNAL_MODE_ORDERED_ID,
+                mActivityInterceptorCallback);
+        mPackageReceiver.register();
+        removeUninstalledPackagesFromSettings();
+    }
+
+    @Override
+    public void finalize() {
+        mPackageReceiver.unregister();
+    }
+
+    private Set<String> getUserEnabledApps() {
         final String encodedApps = Settings.Secure.getStringForUser(
                 mContext.getContentResolver(),
                 Settings.Secure.COMMUNAL_MODE_PACKAGES,
                 UserHandle.USER_SYSTEM);
 
-        mEnabledApps.clear();
+        return TextUtils.isEmpty(encodedApps)
+                ? Collections.emptySet()
+                : new HashSet<>(Arrays.asList(encodedApps.split(DELIMITER)));
+    }
 
-        if (!TextUtils.isEmpty(encodedApps)) {
-            mEnabledApps.addAll(Arrays.asList(encodedApps.split(DELIMITER)));
+    private void removeUninstalledPackagesFromSettings() {
+        for (String packageName : getUserEnabledApps()) {
+            if (!isPackageInstalled(packageName, mPackageManager)) {
+                removePackageFromSettings(packageName);
+            }
         }
     }
 
-    private boolean isActivityAllowed(ActivityInfo activityInfo) {
-        return true;
-        // TODO(b/191994709): Uncomment the lines below once Dreams and Assistant have been fixed.
-//        if (!mCommunalViewIsShowing.get() || !mKeyguardManager.isKeyguardLocked()) return true;
-//
-//        // If the activity doesn't have showWhenLocked enabled, disallow the activity.
-//        final boolean showWhenLocked =
-//                (activityInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0;
-//        if (!showWhenLocked) {
-//            return false;
-//        }
-//
-//        // Check the cached user preferences to see if the user has allowed this app.
-//        return mEnabledApps.contains(activityInfo.applicationInfo.packageName);
+    private void removePackageFromSettings(String packageName) {
+        Set<String> enabledPackages = getUserEnabledApps();
+        if (enabledPackages.remove(packageName)) {
+            Settings.Secure.putStringForUser(
+                    mContext.getContentResolver(),
+                    Settings.Secure.COMMUNAL_MODE_PACKAGES,
+                    String.join(DELIMITER, enabledPackages),
+                    UserHandle.USER_SYSTEM);
+        }
     }
 
-    private final class SettingsObserver extends ContentObserver {
-        SettingsObserver() {
-            super(mHandler);
+    @VisibleForTesting
+    static boolean isPackageInstalled(String packageName, PackageManager packageManager) {
+        if (packageManager == null) return false;
+        try {
+            return packageManager.getPackageInfo(packageName, 0) != null;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    private boolean isAppAllowed(ApplicationInfo appInfo) {
+        if (isActiveDream(appInfo) || isChangeEnabled(ALLOW_COMMUNAL_MODE_BY_DEFAULT, appInfo)) {
+            return true;
         }
 
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            mContext.getMainExecutor().execute(CommunalManagerService.this::updateSelectedApps);
-        }
+        return isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, appInfo)
+                && getUserEnabledApps().contains(appInfo.packageName);
+    }
+
+    private boolean isActiveDream(ApplicationInfo appInfo) {
+        final ComponentName activeDream = mDreamManagerInternal.getActiveDreamComponent(
+                /* doze= */ false);
+        final ComponentName activeDoze = mDreamManagerInternal.getActiveDreamComponent(
+                /* doze= */ true);
+        return isFromPackage(activeDream, appInfo) || isFromPackage(activeDoze, appInfo);
+    }
+
+    private static boolean isFromPackage(ComponentName componentName, ApplicationInfo appInfo) {
+        if (componentName == null) return false;
+        return TextUtils.equals(appInfo.packageName, componentName.getPackageName());
+    }
+
+    private static boolean isChangeEnabled(long changeId, ApplicationInfo appInfo) {
+        return CompatChanges.isChangeEnabled(changeId, appInfo.packageName, UserHandle.SYSTEM);
+    }
+
+    private boolean shouldIntercept(ActivityInfo activityInfo) {
+        if (!mCommunalViewIsShowing.get() || !mKeyguardManager.isKeyguardLocked()) return false;
+        return !isAppAllowed(activityInfo.applicationInfo);
     }
 
     private final class BinderService extends ICommunalManager.Stub {
@@ -168,4 +252,49 @@
             mCommunalViewIsShowing.set(isShowing);
         }
     }
+
+    /**
+     * A {@link BroadcastReceiver} that listens on package removed events and updates any stored
+     * package state in Settings.
+     */
+    private final class PackageReceiver extends BroadcastReceiver {
+        private final Context mContext;
+        private final IntentFilter mIntentFilter;
+
+        private PackageReceiver(Context context) {
+            mContext = context;
+            mIntentFilter = new IntentFilter();
+            mIntentFilter.addAction(ACTION_PACKAGE_REMOVED);
+            mIntentFilter.addDataScheme("package");
+        }
+
+        private void register() {
+            mContext.registerReceiverAsUser(
+                    this,
+                    UserHandle.SYSTEM,
+                    mIntentFilter,
+                    /* broadcastPermission= */null,
+                    /* scheduler= */ null);
+        }
+
+        private void unregister() {
+            mContext.unregisterReceiver(this);
+        }
+
+        @Override
+        public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
+            final Uri data = intent.getData();
+            if (data == null) {
+                Slog.w(TAG, "Failed to get package name in package receiver");
+                return;
+            }
+            final String packageName = data.getSchemeSpecificPart();
+            final String action = intent.getAction();
+            if (ACTION_PACKAGE_REMOVED.equals(action)) {
+                removePackageFromSettings(packageName);
+            } else {
+                Slog.w(TAG, "Unsupported action in package receiver: " + action);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/communal/OWNERS b/services/core/java/com/android/server/communal/OWNERS
new file mode 100644
index 0000000..e499b160
--- /dev/null
+++ b/services/core/java/com/android/server/communal/OWNERS
@@ -0,0 +1,3 @@
+brycelee@google.com
+justinkoh@google.com
+lusilva@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 768587a..2f3342f 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -378,6 +378,7 @@
     private float mInitialAutoBrightness;
 
     // The controller for the automatic brightness level.
+    @Nullable
     private AutomaticBrightnessController mAutomaticBrightnessController;
 
     private Sensor mLightSensor;
@@ -608,7 +609,7 @@
         mPendingRbcOnOrChanged = strengthChanged || justActivated;
 
         // Reset model if strength changed OR rbc is turned off
-        if (strengthChanged || !justActivated && mAutomaticBrightnessController != null) {
+        if ((strengthChanged || !justActivated) && mAutomaticBrightnessController != null) {
             mAutomaticBrightnessController.resetShortTermModel();
         }
     }
@@ -1567,7 +1568,9 @@
                     sendUpdatePowerStateLocked();
                     mHandler.post(mOnBrightnessChangeRunnable);
                     // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern.
-                    mAutomaticBrightnessController.update();
+                    if (mAutomaticBrightnessController != null) {
+                        mAutomaticBrightnessController.update();
+                    }
                 }, mContext);
     }
 
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 9390284..5c3a991 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -49,8 +49,6 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
-import com.android.server.apphibernation.AppHibernationManagerInternal;
-import com.android.server.apphibernation.AppHibernationService;
 import com.android.server.pm.dex.DexManager;
 import com.android.server.pm.dex.DexoptOptions;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
@@ -171,7 +169,7 @@
                 }
             }
 
-            if (!PackageDexOptimizer.canOptimizePackage(pkg)) {
+            if (!mPm.mPackageDexOptimizer.canOptimizePackage(pkg)) {
                 if (DEBUG_DEXOPT) {
                     Log.i(TAG, "Skipping update of non-optimizable app " + pkg.getPackageName());
                 }
@@ -291,16 +289,11 @@
         ArraySet<String> pkgs = new ArraySet<>();
         synchronized (mPm.mLock) {
             for (AndroidPackage p : mPm.mPackages.values()) {
-                if (PackageDexOptimizer.canOptimizePackage(p)) {
+                if (mPm.mPackageDexOptimizer.canOptimizePackage(p)) {
                     pkgs.add(p.getPackageName());
                 }
             }
         }
-        if (AppHibernationService.isAppHibernationEnabled()) {
-            AppHibernationManagerInternal appHibernationManager =
-                    mPm.mInjector.getLocalService(AppHibernationManagerInternal.class);
-            pkgs.removeIf(pkgName -> appHibernationManager.isHibernatingGlobally(pkgName));
-        }
         return pkgs;
     }
 
diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java
index 0581e72..61a6ed4 100644
--- a/services/core/java/com/android/server/pm/OtaDexoptService.java
+++ b/services/core/java/com/android/server/pm/OtaDexoptService.java
@@ -388,7 +388,7 @@
             }
 
             // Does the package have code? If not, there won't be any artifacts.
-            if (!PackageDexOptimizer.canOptimizePackage(pkg)) {
+            if (!mPackageManagerService.mPackageDexOptimizer.canOptimizePackage(pkg)) {
                 continue;
             }
             if (pkg.getPath() == null) {
diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
index 7b829a2..e845aec 100644
--- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java
+++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java
@@ -64,7 +64,10 @@
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.apphibernation.AppHibernationManagerInternal;
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.pm.dex.ArtManagerService;
 import com.android.server.pm.dex.ArtStatsLogUtils;
@@ -135,16 +138,24 @@
     private volatile boolean mSystemReady;
 
     private final ArtStatsLogger mArtStatsLogger = new ArtStatsLogger();
+    private final Injector mInjector;
+
 
     private static final Random sRandom = new Random();
 
     PackageDexOptimizer(Installer installer, Object installLock, Context context,
             String wakeLockTag) {
-        this.mInstaller = installer;
-        this.mInstallLock = installLock;
+        this(new Injector() {
+            @Override
+            public AppHibernationManagerInternal getAppHibernationManagerInternal() {
+                return LocalServices.getService(AppHibernationManagerInternal.class);
+            }
 
-        PowerManager powerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
-        mDexoptWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakeLockTag);
+            @Override
+            public PowerManager getPowerManager(Context context) {
+                return context.getSystemService(PowerManager.class);
+            }
+        }, installer, installLock, context, wakeLockTag);
     }
 
     protected PackageDexOptimizer(PackageDexOptimizer from) {
@@ -152,9 +163,21 @@
         this.mInstallLock = from.mInstallLock;
         this.mDexoptWakeLock = from.mDexoptWakeLock;
         this.mSystemReady = from.mSystemReady;
+        this.mInjector = from.mInjector;
     }
 
-    static boolean canOptimizePackage(AndroidPackage pkg) {
+    @VisibleForTesting
+    PackageDexOptimizer(@NonNull Injector injector, Installer installer, Object installLock,
+            Context context, String wakeLockTag) {
+        this.mInstaller = installer;
+        this.mInstallLock = installLock;
+
+        PowerManager powerManager = injector.getPowerManager(context);
+        mDexoptWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, wakeLockTag);
+        mInjector = injector;
+    }
+
+    boolean canOptimizePackage(AndroidPackage pkg) {
         // We do not dexopt a package with no code.
         // Note that the system package is marked as having no code, however we can
         // still optimize it via dexoptSystemServerPath.
@@ -162,6 +185,17 @@
             return false;
         }
 
+        // We do not dexopt unused packages.
+        // It's possible for this to be called before app hibernation service is ready due to
+        // an OTA dexopt. In this case, we ignore the hibernation check here. This is fine since
+        // a hibernating app should have no artifacts to copy in the first place.
+        AppHibernationManagerInternal ahm = mInjector.getAppHibernationManagerInternal();
+        if (ahm != null
+                && ahm.isHibernatingGlobally(pkg.getPackageName())
+                && ahm.isOatArtifactDeletionEnabled()) {
+            return false;
+        }
+
         return true;
     }
 
@@ -1002,4 +1036,13 @@
     private Installer getInstallerWithoutLock() {
         return mInstaller;
     }
+
+    /**
+     * Injector for {@link PackageDexOptimizer} dependencies
+     */
+    interface Injector {
+        AppHibernationManagerInternal getAppHibernationManagerInternal();
+
+        PowerManager getPowerManager(Context context);
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 314a201..7fd7505 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -392,6 +392,9 @@
     @GuardedBy("mLock")
     private int mParentSessionId;
 
+    @GuardedBy("mLock")
+    private boolean mHasDeviceAdminReceiver;
+
     static class FileEntry {
         private final int mIndex;
         private final InstallationFile mFile;
@@ -927,11 +930,13 @@
     @UserActionRequirement
     private int computeUserActionRequirement() {
         final String packageName;
+        final boolean hasDeviceAdminReceiver;
         synchronized (mLock) {
             if (mPermissionsManuallyAccepted) {
                 return USER_ACTION_NOT_NEEDED;
             }
             packageName = mPackageName;
+            hasDeviceAdminReceiver = mHasDeviceAdminReceiver;
         }
 
         final boolean forcePermissionPrompt =
@@ -954,6 +959,9 @@
         final boolean isUpdateWithoutUserActionPermissionGranted = (mPm.checkUidPermission(
                 android.Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION, mInstallerUid)
                 == PackageManager.PERMISSION_GRANTED);
+        final boolean isInstallDpcPackagesPermissionGranted = (mPm.checkUidPermission(
+                android.Manifest.permission.INSTALL_DPC_PACKAGES, mInstallerUid)
+                == PackageManager.PERMISSION_GRANTED);
         final int targetPackageUid = mPm.getPackageUid(packageName, 0, userId);
         final boolean isUpdate = targetPackageUid != -1 || isApexSession();
         final InstallSourceInfo existingInstallSourceInfo = isUpdate
@@ -967,7 +975,8 @@
         final boolean isSelfUpdate = targetPackageUid == mInstallerUid;
         final boolean isPermissionGranted = isInstallPermissionGranted
                 || (isUpdatePermissionGranted && isUpdate)
-                || (isSelfUpdatePermissionGranted && isSelfUpdate);
+                || (isSelfUpdatePermissionGranted && isSelfUpdate)
+                || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver);
         final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID);
         final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID);
 
@@ -2854,6 +2863,7 @@
         }
 
         mSigningDetails = apk.getSigningDetails();
+        mHasDeviceAdminReceiver = apk.isHasDeviceAdminReceiver();
     }
 
     /**
@@ -2944,6 +2954,7 @@
             if (mSigningDetails == SigningDetails.UNKNOWN) {
                 mSigningDetails = apk.getSigningDetails();
             }
+            mHasDeviceAdminReceiver = apk.isHasDeviceAdminReceiver();
 
             assertApkConsistentLocked(String.valueOf(addedFile), apk);
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 1c9f649..d8a1320 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2710,7 +2710,7 @@
         final ActivityInfo.WindowLayout windowLayout = info.windowLayout;
         return windowLayout == null
                 || tda.supportsActivityMinWidthHeightMultiWindow(windowLayout.minWidth,
-                windowLayout.minHeight);
+                windowLayout.minHeight, info);
     }
 
     /**
@@ -5695,7 +5695,7 @@
                     destroyImmediately("stop-config");
                     mRootWindowContainer.resumeFocusedTasksTopActivities();
                 } else {
-                    mRootWindowContainer.updatePreviousProcess(this);
+                    mAtmService.updatePreviousProcess(this);
                 }
             }
             mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 01a5636..9455ce6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -398,7 +398,7 @@
      */
     volatile WindowProcessController mPreviousProcess;
     /** The time at which the previous process was last visible. */
-    long mPreviousProcessVisibleTime;
+    private long mPreviousProcessVisibleTime;
 
     /** List of intents that were used to start the most recent tasks. */
     private RecentTasks mRecentTasks;
@@ -4634,6 +4634,23 @@
         mTopApp = top != null ? top.app : null;
     }
 
+    /**
+     * The process state of previous activity is more important than the regular background to keep
+     * around in case the user wants to return to it.
+     */
+    void updatePreviousProcess(ActivityRecord stoppedActivity) {
+        if (stoppedActivity.app != null && mTopApp != null
+                // Don't replace the previous process if the stopped one is the top, e.g. sleeping.
+                && stoppedActivity.app != mTopApp
+                // The stopped activity must have been visible later than the previous.
+                && stoppedActivity.lastVisibleTime > mPreviousProcessVisibleTime
+                // Home has its own retained state, so don't let it occupy the previous.
+                && stoppedActivity.app != mHomeProcess) {
+            mPreviousProcess = stoppedActivity.app;
+            mPreviousProcessVisibleTime = stoppedActivity.lastVisibleTime;
+        }
+    }
+
     void updateActivityUsageStats(ActivityRecord activity, int event) {
         ComponentName taskRoot = null;
         final Task task = activity.getTask();
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index b35d84b..371cfc5 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -147,6 +147,7 @@
 import com.android.server.LocalServices;
 import com.android.server.am.ActivityManagerService;
 import com.android.server.am.UserState;
+import com.android.server.utils.Slogf;
 import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
 
 import java.io.FileDescriptor;
@@ -1322,8 +1323,6 @@
             // us, we can now deliver.
             r.idle = true;
 
-            //Slog.i(TAG, "IDLE: mBooted=" + mBooted + ", fromTimeout=" + fromTimeout);
-
             // Check if able to finish booting when device is booting and all resumed activities
             // are idle.
             if ((mService.isBooting() && mRootWindowContainer.allResumedActivitiesIdle())
@@ -1356,14 +1355,21 @@
         // Atomically retrieve all of the other things to do.
         processStoppingAndFinishingActivities(r, processPausingActivities, "idle");
 
+        if (DEBUG_IDLE) {
+            Slogf.i(TAG, "activityIdleInternal(): r=%s, booting=%b, mStartingUsers=%s", r, booting,
+                    mStartingUsers);
+        }
+
         if (!mStartingUsers.isEmpty()) {
             final ArrayList<UserState> startingUsers = new ArrayList<>(mStartingUsers);
             mStartingUsers.clear();
-
-            if (!booting) {
+            // TODO(b/190854171): remove the isHeadlessSystemUserMode() check on master
+            if (!booting || UserManager.isHeadlessSystemUserMode()) {
                 // Complete user switch.
                 for (int i = 0; i < startingUsers.size(); i++) {
-                    mService.mAmInternal.finishUserSwitch(startingUsers.get(i));
+                    UserState userState = startingUsers.get(i);
+                    Slogf.i(TAG, "finishing switch of user %d", userState.mHandle.getIdentifier());
+                    mService.mAmInternal.finishUserSwitch(userState);
                 }
             }
         }
@@ -2012,6 +2018,7 @@
 
     final void scheduleIdle() {
         if (!mHandler.hasMessages(IDLE_NOW_MSG)) {
+            if (DEBUG_IDLE) Slog.d(TAG_IDLE, "scheduleIdle: Callers=" + Debug.getCallers(4));
             mHandler.sendEmptyMessage(IDLE_NOW_MSG);
         }
     }
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index ee05523..6d96cf0 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -473,7 +473,8 @@
      */
     static void notifyAnimationCancelBeforeStart(IRecentsAnimationRunner recentsAnimationRunner) {
         try {
-            recentsAnimationRunner.onAnimationCanceled(null /* taskSnapshot */);
+            recentsAnimationRunner.onAnimationCanceled(null /* taskIds */,
+                    null /* taskSnapshots */);
         } catch (RemoteException e) {
             Slog.e(TAG, "Failed to cancel recents animation before start", e);
         }
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index 918eee9..634f489 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -852,37 +852,37 @@
             mCanceled = true;
 
             if (screenshot && !mPendingAnimations.isEmpty()) {
-                final TaskAnimationAdapter adapter = mPendingAnimations.get(0);
-                final Task task = adapter.mTask;
-                // Screen shot previous task when next task starts transition and notify the runner.
-                // We will actually finish the animation once the runner calls cleanUpScreenshot().
-                final TaskSnapshot taskSnapshot = screenshotRecentTask(task);
+                final ArrayMap<Task, TaskSnapshot> snapshotMap = screenshotRecentTasks();
                 mPendingCancelWithScreenshotReorderMode = reorderMode;
-                try {
-                    mRunner.onAnimationCanceled(taskSnapshot);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Failed to cancel recents animation", e);
-                }
-                if (taskSnapshot != null) {
-                    // Defer until the runner calls back to cleanupScreenshot()
-                    adapter.setSnapshotOverlay(taskSnapshot);
+
+                if (!snapshotMap.isEmpty()) {
+                    try {
+                        int[] taskIds = new int[snapshotMap.size()];
+                        TaskSnapshot[] snapshots = new TaskSnapshot[snapshotMap.size()];
+                        for (int i = snapshotMap.size() - 1; i >= 0; i--) {
+                            taskIds[i] = snapshotMap.keyAt(i).mTaskId;
+                            snapshots[i] = snapshotMap.valueAt(i);
+                        }
+                        mRunner.onAnimationCanceled(taskIds, snapshots);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Failed to cancel recents animation", e);
+                    }
                     // Schedule a new failsafe for if the runner doesn't clean up the screenshot
                     scheduleFailsafe();
-                } else {
-                    // Do a normal cancel since we couldn't screenshot
-                    mCallbacks.onAnimationFinished(reorderMode, false /* sendUserLeaveHint */);
+                    return;
                 }
-            } else {
-                // Otherwise, notify the runner and clean up the animation immediately
-                // Note: In the fallback case, this can trigger multiple onAnimationCancel() calls
-                // to the runner if we this actually triggers cancel twice on the caller
-                try {
-                    mRunner.onAnimationCanceled(null /* taskSnapshot */);
-                } catch (RemoteException e) {
-                    Slog.e(TAG, "Failed to cancel recents animation", e);
-                }
-                mCallbacks.onAnimationFinished(reorderMode, false /* sendUserLeaveHint */);
+                // Fallback to a normal cancel since we couldn't screenshot
             }
+
+            // Notify the runner and clean up the animation immediately
+            // Note: In the fallback case, this can trigger multiple onAnimationCancel() calls
+            // to the runner if we this actually triggers cancel twice on the caller
+            try {
+                mRunner.onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to cancel recents animation", e);
+            }
+            mCallbacks.onAnimationFinished(reorderMode, false /* sendUserLeaveHint */);
         }
     }
 
@@ -956,13 +956,23 @@
         return mRequestDeferCancelUntilNextTransition && mCancelDeferredWithScreenshot;
     }
 
-    TaskSnapshot screenshotRecentTask(Task task) {
+    private ArrayMap<Task, TaskSnapshot> screenshotRecentTasks() {
         final TaskSnapshotController snapshotController = mService.mTaskSnapshotController;
-        final ArraySet<Task> tasks = Sets.newArraySet(task);
-        snapshotController.snapshotTasks(tasks);
-        snapshotController.addSkipClosingAppSnapshotTasks(tasks);
-        return snapshotController.getSnapshot(task.mTaskId, task.mUserId,
-                false /* restoreFromDisk */, false /* isLowResolution */);
+        final ArrayMap<Task, TaskSnapshot> snapshotMap = new ArrayMap<>();
+        for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+            final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
+            final Task task = adapter.mTask;
+            snapshotController.recordTaskSnapshot(task, false /* allowSnapshotHome */);
+            final TaskSnapshot snapshot = snapshotController.getSnapshot(task.mTaskId, task.mUserId,
+                    false /* restoreFromDisk */, false /* isLowResolution */);
+            if (snapshot != null) {
+                snapshotMap.put(task, snapshot);
+                // Defer until the runner calls back to cleanupScreenshot()
+                adapter.setSnapshotOverlay(snapshot);
+            }
+        }
+        snapshotController.addSkipClosingAppSnapshotTasks(snapshotMap.keySet());
+        return snapshotMap;
     }
 
     void cleanupAnimation(@ReorderMode int reorderMode) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index bd39e00..c28d089 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1894,34 +1894,6 @@
         return task != null && task == getTopDisplayFocusedRootTask();
     }
 
-    void updatePreviousProcess(ActivityRecord r) {
-        // Now that this process has stopped, we may want to consider it to be the previous app to
-        // try to keep around in case the user wants to return to it.
-
-        // First, found out what is currently the foreground app, so that we don't blow away the
-        // previous app if this activity is being hosted by the process that is actually still the
-        // foreground.
-        WindowProcessController fgApp = getItemFromRootTasks(rootTask -> {
-            if (isTopDisplayFocusedRootTask(rootTask)) {
-                final ActivityRecord resumedActivity = rootTask.getTopResumedActivity();
-                if (resumedActivity != null) {
-                    return resumedActivity.app;
-                } else if (rootTask.getTopPausingActivity() != null) {
-                    return rootTask.getTopPausingActivity().app;
-                }
-            }
-            return null;
-        });
-
-        // Now set this one as the previous process, only if that really makes sense to.
-        if (r.hasProcess() && fgApp != null && r.app != fgApp
-                && r.lastVisibleTime > mService.mPreviousProcessVisibleTime
-                && r.app != mService.mHomeProcess) {
-            mService.mPreviousProcess = r.app;
-            mService.mPreviousProcessVisibleTime = r.lastVisibleTime;
-        }
-    }
-
     boolean attachApplication(WindowProcessController app) throws RemoteException {
         try {
             return mAttachApplicationHelper.process(app);
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index d712bbf..50c9b31 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -188,6 +188,10 @@
         mAnimation.startAnimation(mLeash, t, type, mInnerAnimationFinishedCallback);
         if (snapshotAnim != null) {
             mSnapshot = freezer.takeSnapshotForAnimation();
+            if (mSnapshot == null) {
+                Slog.e(TAG, "No snapshot target to start animation on for " + mAnimatable);
+                return;
+            }
             mSnapshot.startAnimation(t, snapshotAnim, type);
         }
     }
diff --git a/services/core/java/com/android/server/wm/SurfaceFreezer.java b/services/core/java/com/android/server/wm/SurfaceFreezer.java
index c667db8..a7ef36b 100644
--- a/services/core/java/com/android/server/wm/SurfaceFreezer.java
+++ b/services/core/java/com/android/server/wm/SurfaceFreezer.java
@@ -26,6 +26,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
+import android.util.Slog;
 import android.view.SurfaceControl;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -49,8 +50,10 @@
  */
 class SurfaceFreezer {
 
-    private final Freezable mAnimatable;
-    private final WindowManagerService mWmService;
+    private static final String TAG = "SurfaceFreezer";
+
+    private final @NonNull Freezable mAnimatable;
+    private final @NonNull WindowManagerService mWmService;
     @VisibleForTesting
     SurfaceControl mLeash;
     Snapshot mSnapshot = null;
@@ -59,7 +62,7 @@
     /**
      * @param animatable The object to animate.
      */
-    SurfaceFreezer(Freezable animatable, WindowManagerService service) {
+    SurfaceFreezer(@NonNull Freezable animatable, @NonNull WindowManagerService service) {
         mAnimatable = animatable;
         mWmService = service;
     }
@@ -75,6 +78,7 @@
      */
     void freeze(SurfaceControl.Transaction t, Rect startBounds, Point relativePosition,
             @Nullable SurfaceControl freezeTarget) {
+        reset(t);
         mFreezeBounds.set(startBounds);
 
         mLeash = SurfaceAnimator.createAnimationLeash(mAnimatable, mAnimatable.getSurfaceControl(),
@@ -85,11 +89,14 @@
 
         freezeTarget = freezeTarget != null ? freezeTarget : mAnimatable.getFreezeSnapshotTarget();
         if (freezeTarget != null) {
-            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = createSnapshotBuffer(
+            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = createSnapshotBufferInner(
                     freezeTarget, startBounds);
             final HardwareBuffer buffer = screenshotBuffer == null ? null
                     : screenshotBuffer.getHardwareBuffer();
             if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
+                // This can happen when display is not ready.
+                Slog.w(TAG, "Failed to capture screenshot for " + mAnimatable);
+                unfreeze(t);
                 return;
             }
             mSnapshot = new Snapshot(t, screenshotBuffer, mLeash);
@@ -123,6 +130,11 @@
      * snapshot.
      */
     void unfreeze(SurfaceControl.Transaction t) {
+        unfreezeInner(t);
+        mAnimatable.onUnfrozen();
+    }
+
+    private void unfreezeInner(SurfaceControl.Transaction t) {
         if (mSnapshot != null) {
             mSnapshot.cancelAnimation(t, false /* restarting */);
             mSnapshot = null;
@@ -139,6 +151,22 @@
         }
     }
 
+    /** Resets the snapshot before taking another one if the animation hasn't been started yet. */
+    private void reset(SurfaceControl.Transaction t) {
+        // Those would have been taken by the SurfaceAnimator if the animation has been started, so
+        // we can remove the leash directly.
+        // No need to reset the mAnimatable leash, as this is called before a new animation leash is
+        // created, so another #onAnimationLeashCreated will be called.
+        if (mSnapshot != null) {
+            mSnapshot.destroy(t);
+            mSnapshot = null;
+        }
+        if (mLeash != null) {
+            t.remove(mLeash);
+            mLeash = null;
+        }
+    }
+
     void setLayer(SurfaceControl.Transaction t, int layer) {
         if (mLeash != null) {
             t.setLayer(mLeash, layer);
@@ -171,6 +199,18 @@
         return SurfaceControl.captureLayers(captureArgs);
     }
 
+    @VisibleForTesting
+    SurfaceControl.ScreenshotHardwareBuffer createSnapshotBufferInner(
+            SurfaceControl target, Rect bounds) {
+        return createSnapshotBuffer(target, bounds);
+    }
+
+    @VisibleForTesting
+    GraphicBuffer createFromHardwareBufferInner(
+            SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer) {
+        return GraphicBuffer.createFromHardwareBuffer(screenshotBuffer.getHardwareBuffer());
+    }
+
     class Snapshot {
         private SurfaceControl mSurfaceControl;
         private AnimationAdapter mAnimation;
@@ -181,10 +221,7 @@
          */
         Snapshot(SurfaceControl.Transaction t,
                 SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer, SurfaceControl parent) {
-            // We can't use a delegating constructor since we need to
-            // reference this::onAnimationFinished
-            GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(
-                    screenshotBuffer.getHardwareBuffer());
+            GraphicBuffer graphicBuffer = createFromHardwareBufferInner(screenshotBuffer);
 
             mSurfaceControl = mAnimatable.makeAnimationLeash()
                     .setName("snapshot anim: " + mAnimatable.toString())
@@ -258,5 +295,8 @@
          *         will be generated (but the rest of the freezing logic will still happen).
          */
         @Nullable SurfaceControl getFreezeSnapshotTarget();
+
+        /** Called when the {@link #unfreeze(SurfaceControl.Transaction)} is called. */
+        void onUnfrozen();
     }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index e43c9d9..367feac 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -217,7 +217,6 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
-import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -3226,12 +3225,6 @@
         return isRootTask() && callback.test(this) ? this : null;
     }
 
-    @Nullable
-    @Override
-    <R> R getItemFromRootTasks(Function<Task, R> callback, boolean traverseTopToBottom) {
-        return isRootTask() ? callback.apply(this) : null;
-    }
-
     /**
      * @param canAffectSystemUiFlags If false, all windows in this task can not affect SystemUI
      *                               flags. See {@link WindowState#canAffectSystemUiFlags()}.
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 7482f4e..4d986a2 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -46,6 +46,7 @@
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
 import android.app.WindowConfiguration;
+import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.os.UserHandle;
 import android.util.IntArray;
@@ -1633,13 +1634,17 @@
      * Whether we can show activity requesting the given min width/height in multi window below
      * this {@link TaskDisplayArea}.
      */
-    boolean supportsActivityMinWidthHeightMultiWindow(int minWidth, int minHeight) {
-        final int configRespectsActivityMinWidthHeightMultiWindow =
-                mAtmService.mRespectsActivityMinWidthHeightMultiWindow;
+    boolean supportsActivityMinWidthHeightMultiWindow(int minWidth, int minHeight,
+            @Nullable ActivityInfo activityInfo) {
+        if (activityInfo != null && !activityInfo.shouldCheckMinWidthHeightForMultiWindow()) {
+            return true;
+        }
         if (minWidth <= 0 && minHeight <= 0) {
             // No request min width/height.
             return true;
         }
+        final int configRespectsActivityMinWidthHeightMultiWindow =
+                mAtmService.mRespectsActivityMinWidthHeightMultiWindow;
         if (configRespectsActivityMinWidthHeightMultiWindow == -1) {
             // Device override to ignore min width/height.
             return true;
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 659179c..49bbd8a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1768,7 +1768,9 @@
             return false;
         }
 
-        return tda.supportsActivityMinWidthHeightMultiWindow(mMinWidth, mMinHeight);
+        final ActivityRecord rootActivity = getTask().getRootActivity();
+        return tda.supportsActivityMinWidthHeightMultiWindow(mMinWidth, mMinHeight,
+                rootActivity != null ? rootActivity.info : null);
     }
 
     private int getTaskId() {
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 6aa707c..81b7cbe 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -69,6 +69,7 @@
 import com.google.android.collect.Sets;
 
 import java.io.PrintWriter;
+import java.util.Set;
 
 /**
  * When an app token becomes invisible, we take a snapshot (bitmap) of the corresponding task and
@@ -184,7 +185,7 @@
      * calling {@link #snapshotTasks} to ensure that the task has an up-to-date snapshot.
      */
     @VisibleForTesting
-    void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
+    void addSkipClosingAppSnapshotTasks(Set<Task> tasks) {
         if (shouldDisableSnapshots()) {
             return;
         }
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 225eeec..9865506 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -614,7 +614,6 @@
         final DisplayContent dc = getDisplayContent();
         if (dc != null) {
             mSurfaceFreezer.unfreeze(getSyncTransaction());
-            dc.mChangingContainers.remove(this);
         }
         while (!mChildren.isEmpty()) {
             final E child = mChildren.peekLast();
@@ -1052,9 +1051,6 @@
         // part of a change transition.
         if (!visible) {
             mSurfaceFreezer.unfreeze(getSyncTransaction());
-            if (mDisplayContent != null) {
-                mDisplayContent.mChangingContainers.remove(this);
-            }
         }
         WindowContainer parent = getParent();
         if (parent != null) {
@@ -2117,47 +2113,6 @@
     }
 
     /**
-     * Finds the first non {@code null} return value from calling the callback on all root
-     * {@link Task} at or below this container.
-     * @param callback Applies on each root {@link Task} found and stops the search if it
-     *                 returns non {@code null}.
-     * @param traverseTopToBottom If {@code true}, traverses the hierarchy from top-to-bottom in
-     *                            terms of z-order, else from bottom-to-top.
-     * @return the first returned object that is not {@code null}. Returns {@code null} if not
-     *         found.
-     */
-    @Nullable
-    <R> R getItemFromRootTasks(Function<Task, R> callback, boolean traverseTopToBottom) {
-        int count = mChildren.size();
-        if (traverseTopToBottom) {
-            for (int i = count - 1; i >= 0; --i) {
-                R result = (R) mChildren.get(i).getItemFromRootTasks(callback, traverseTopToBottom);
-                if (result != null) {
-                    return result;
-                }
-            }
-        } else {
-            for (int i = 0; i < count; i++) {
-                R result = (R) mChildren.get(i).getItemFromRootTasks(callback, traverseTopToBottom);
-                if (result != null) {
-                    return result;
-                }
-                // Root tasks may be removed from this display. Ensure each task will be processed
-                // and the loop will end.
-                int newCount = mChildren.size();
-                i -= count - newCount;
-                count = newCount;
-            }
-        }
-        return null;
-    }
-
-    @Nullable
-    <R> R getItemFromRootTasks(Function<Task, R> callback) {
-        return getItemFromRootTasks(callback, true /* traverseTopToBottom */);
-    }
-
-    /**
      * Returns 1, 0, or -1 depending on if this container is greater than, equal to, or lesser than
      * the input container in terms of z-order.
      */
@@ -2632,6 +2587,13 @@
     }
 
     @Override
+    public void onUnfrozen() {
+        if (mDisplayContent != null) {
+            mDisplayContent.mChangingContainers.remove(this);
+        }
+    }
+
+    @Override
     public Builder makeAnimationLeash() {
         return makeSurface().setContainerLayer();
     }
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index aa258e8..6be872f 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -1664,6 +1664,13 @@
         }
     }
 
+    auto gnssGeofencing = gnssHal->getExtensionGnssGeofencing();
+    if (!gnssGeofencing.isOk()) {
+        ALOGD("Unable to get a handle to GnssGeofencing");
+    } else {
+        gnssGeofencingIface = gnssGeofencing;
+    }
+
     if (gnssHalAidl != nullptr && gnssHalAidl->getInterfaceVersion() >= 2) {
         sp<IGnssBatchingAidl> gnssBatchingAidl;
         auto status = gnssHalAidl->getExtensionGnssBatching(&gnssBatchingAidl);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportManager.java
index d7cbd9b..4620ea6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/RemoteBugreportManager.java
@@ -219,7 +219,8 @@
         try {
             final IntentFilter filterFinished =
                     new IntentFilter(ACTION_REMOTE_BUGREPORT_DISPATCH, BUGREPORT_MIMETYPE);
-            mContext.registerReceiver(mRemoteBugreportFinishedReceiver, filterFinished);
+            mContext.registerReceiver(mRemoteBugreportFinishedReceiver, filterFinished,
+                    Context.RECEIVER_EXPORTED);
         } catch (IntentFilter.MalformedMimeTypeException e) {
             // should never happen, as setting a constant
             Slogf.w(LOG_TAG, e, "Failed to set type %s", BUGREPORT_MIMETYPE);
diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
index fdf23d3..4fb801e 100644
--- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
+++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java
@@ -23,7 +23,6 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ResolveInfo;
 import android.os.Handler;
 import android.os.IBinder.DeathRecipient;
 import android.os.Looper;
@@ -32,8 +31,6 @@
 import android.os.SystemProperties;
 import android.os.UpdateEngine;
 import android.os.UpdateEngineCallback;
-import android.os.UserHandle;
-import android.os.UserManager;
 import android.provider.DeviceConfig;
 import android.util.Log;
 
@@ -45,9 +42,6 @@
 import com.android.server.wm.ActivityMetricsLaunchObserverRegistry;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.List;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
 
@@ -306,79 +300,27 @@
             return;
         }
 
-        if (!getUploaderEnabledConfig(getContext())) {
-            return;
-        }
-
+        Context context = getContext();
         new Thread(() -> {
             try {
-                Context context = getContext();
-                final String uploaderPkg = getUploaderPackageName(context);
-                final String uploaderAction = getUploaderActionName(context);
-                String reportUuid = mIProfcollect.report();
+                // Prepare profile report
+                String reportName = mIProfcollect.report() + ".zip";
 
-                final int profileId = getBBProfileId();
-                String reportDir = "/data/user/" + profileId
-                        + "/com.google.android.apps.internal.betterbug/cache/";
-                String reportPath = reportDir + reportUuid + ".zip";
-
-                if (!Files.exists(Paths.get(reportDir))) {
-                    Log.i(LOG_TAG, "Destination directory does not exist, abort upload.");
+                if (!context.getResources().getBoolean(
+                        R.bool.config_profcollectReportUploaderEnabled)) {
+                    Log.i(LOG_TAG, "Upload is not enabled.");
                     return;
                 }
 
-                Intent uploadIntent =
-                        new Intent(uploaderAction)
-                        .setPackage(uploaderPkg)
-                        .putExtra("EXTRA_DESTINATION", "PROFCOLLECT")
-                        .putExtra("EXTRA_PACKAGE_NAME", getContext().getPackageName())
-                        .putExtra("EXTRA_PROFILE_PATH", reportPath)
-                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-
-                List<ResolveInfo> receivers =
-                        context.getPackageManager().queryBroadcastReceivers(uploadIntent, 0);
-                if (receivers == null || receivers.isEmpty()) {
-                    Log.i(LOG_TAG, "No one to receive upload intent, abort upload.");
-                    return;
-                }
-                mIProfcollect.copy_report_to_bb(profileId, reportUuid);
-                context.sendBroadcast(uploadIntent);
-                mIProfcollect.delete_report(reportUuid);
+                // Upload the report
+                Intent intent = new Intent()
+                        .setPackage("com.android.shell")
+                        .setAction("com.android.shell.action.PROFCOLLECT_UPLOAD")
+                        .putExtra("filename", reportName);
+                context.sendBroadcast(intent);
             } catch (RemoteException e) {
                 Log.e(LOG_TAG, e.getMessage());
             }
         }).start();
     }
-
-    /**
-     * Get BetterBug's profile ID. It is the work profile ID, if it exists. Otherwise the system
-     * user ID.
-     *
-     * @return BetterBug's profile ID.
-     */
-    private int getBBProfileId() {
-        UserManager userManager = UserManager.get(getContext());
-        int[] profiles = userManager.getProfileIds(UserHandle.USER_SYSTEM, false);
-        for (int p : profiles) {
-            if (userManager.getUserInfo(p).isManagedProfile()) {
-                return p;
-            }
-        }
-        return UserHandle.USER_SYSTEM;
-    }
-
-    private boolean getUploaderEnabledConfig(Context context) {
-        return context.getResources().getBoolean(
-            R.bool.config_profcollectReportUploaderEnabled);
-    }
-
-    private String getUploaderPackageName(Context context) {
-        return context.getResources().getString(
-            R.string.config_defaultProfcollectReportUploaderApp);
-    }
-
-    private String getUploaderActionName(Context context) {
-        return context.getResources().getString(
-            R.string.config_defaultProfcollectReportUploaderAction);
-    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java
index 06e691f..3aeaa42 100644
--- a/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/communal/CommunalManagerServiceTest.java
@@ -16,37 +16,48 @@
 
 package com.android.server.communal;
 
+import static android.content.Intent.ACTION_PACKAGE_REMOVED;
 import static android.content.pm.ActivityInfo.FLAG_SHOW_WHEN_LOCKED;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.communal.CommunalManagerService.ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT;
 import static com.android.server.wm.ActivityInterceptorCallback.COMMUNAL_MODE_ORDERED_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
 
+import android.Manifest;
+import android.annotation.Nullable;
 import android.app.KeyguardManager;
 import android.app.communal.ICommunalManager;
-import android.content.ContentResolver;
-import android.content.Context;
+import android.app.compat.CompatChanges;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContextWrapper;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
-import android.os.IBinder;
+import android.net.Uri;
 import android.os.RemoteException;
-import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 import android.provider.Settings;
+import android.service.dreams.DreamManagerInternal;
+import android.test.mock.MockContentResolver;
 
+import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
+import com.android.server.SystemService;
 import com.android.server.wm.ActivityInterceptorCallback;
 import com.android.server.wm.ActivityTaskManagerInternal;
 
@@ -84,32 +95,37 @@
     @Mock
     private KeyguardManager mKeyguardManager;
     @Mock
-    private Context mMockContext;
-    @Mock
-    private ContentResolver mContentResolver;
+    private DreamManagerInternal mDreamManagerInternal;
 
     private ActivityInterceptorCallback mActivityInterceptorCallback;
+    private BroadcastReceiver mPackageReceiver;
     private ActivityInfo mAInfo;
     private ICommunalManager mBinder;
+    private ContextWrapper mContextSpy;
 
     @Before
     public final void setUp() {
         mMockingSession = mockitoSession()
                 .initMocks(this)
-                .mockStatic(LocalServices.class)
-                .mockStatic(ServiceManager.class)
-                .mockStatic(Settings.Secure.class)
-                .mockStatic(KeyguardManager.class)
+                .spyStatic(CommunalManagerService.class)
+                .mockStatic(CompatChanges.class)
                 .strictness(Strictness.WARN)
                 .startMocking();
 
-        when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
-        when(mMockContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
-        doReturn(mAtmInternal).when(() -> LocalServices.getService(
-                ActivityTaskManagerInternal.class));
+        mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
+        MockContentResolver cr = new MockContentResolver(mContextSpy);
+        cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mContextSpy.getContentResolver()).thenReturn(cr);
 
-        mService = new CommunalManagerService(mMockContext);
-        mService.onStart();
+        when(mContextSpy.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager);
+        addLocalServiceMock(ActivityTaskManagerInternal.class, mAtmInternal);
+        addLocalServiceMock(DreamManagerInternal.class, mDreamManagerInternal);
+
+        doNothing().when(mContextSpy).enforceCallingPermission(
+                eq(Manifest.permission.WRITE_COMMUNAL_STATE), anyString());
+
+        mService = new CommunalManagerService(mContextSpy);
+        mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
 
         ArgumentCaptor<ActivityInterceptorCallback> activityInterceptorCaptor =
                 ArgumentCaptor.forClass(ActivityInterceptorCallback.class);
@@ -117,11 +133,13 @@
                 activityInterceptorCaptor.capture());
         mActivityInterceptorCallback = activityInterceptorCaptor.getValue();
 
-        ArgumentCaptor<IBinder> binderCaptor = ArgumentCaptor.forClass(IBinder.class);
-        verify(() -> ServiceManager.addService(eq(Context.COMMUNAL_MANAGER_SERVICE),
-                binderCaptor.capture(),
-                anyBoolean(), anyInt()));
-        mBinder = ICommunalManager.Stub.asInterface(binderCaptor.getValue());
+        ArgumentCaptor<BroadcastReceiver> packageReceiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mContextSpy).registerReceiverAsUser(packageReceiverCaptor.capture(),
+                eq(UserHandle.SYSTEM), any(), any(), any());
+        mPackageReceiver = packageReceiverCaptor.getValue();
+
+        mBinder = mService.getBinderServiceInstance();
 
         mAInfo = new ActivityInfo();
         mAInfo.applicationInfo = new ApplicationInfo();
@@ -130,11 +148,20 @@
 
     @After
     public void tearDown() {
+        FakeSettingsProvider.clearSettingsProvider();
         if (mMockingSession != null) {
             mMockingSession.finishMocking();
         }
     }
 
+    /**
+     * Creates a mock and registers it to {@link LocalServices}.
+     */
+    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
+        LocalServices.removeServiceForTest(clazz);
+        LocalServices.addService(clazz, mock);
+    }
+
     private ActivityInterceptorCallback.ActivityInterceptorInfo buildActivityInfo(Intent intent) {
         return new ActivityInterceptorCallback.ActivityInterceptorInfo(
                 TEST_REAL_CALLING_UID,
@@ -152,84 +179,172 @@
     }
 
     private void allowPackages(String packages) {
-        doReturn(packages).when(
-                () -> Settings.Secure.getStringForUser(mContentResolver,
-                        Settings.Secure.COMMUNAL_MODE_PACKAGES, UserHandle.USER_SYSTEM));
-        mService.updateSelectedApps();
+        Settings.Secure.putStringForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.COMMUNAL_MODE_PACKAGES, packages, UserHandle.USER_SYSTEM);
+    }
+
+    private String getAllowedPackages() {
+        return Settings.Secure.getStringForUser(mContextSpy.getContentResolver(),
+                Settings.Secure.COMMUNAL_MODE_PACKAGES, UserHandle.USER_SYSTEM);
+    }
+
+    private void assertDoesIntercept() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNotNull();
+    }
+
+    private void assertDoesNotIntercept() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+    }
+
+    private Intent createPackageIntent(String packageName, @Nullable String action) {
+        return new Intent(action, Uri.parse("package:" + packageName));
     }
 
     @Test
     public void testIntercept_unlocked_communalOff_appNotEnabled_showWhenLockedOff() {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
         mAInfo.flags = 0;
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesNotIntercept();
     }
 
     @Test
     public void testIntercept_unlocked_communalOn_appNotEnabled_showWhenLockedOff()
             throws RemoteException {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         mBinder.setCommunalViewShowing(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
         mAInfo.flags = 0;
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesNotIntercept();
     }
 
     @Test
     public void testIntercept_locked_communalOff_appNotEnabled_showWhenLockedOff() {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
         mAInfo.flags = 0;
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesNotIntercept();
     }
 
     @Test
-    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOff()
+    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOff_allowlistEnabled()
             throws RemoteException {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         mBinder.setCommunalViewShowing(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
+                UserHandle.SYSTEM)).thenReturn(true);
         mAInfo.flags = 0;
-        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesIntercept();
     }
 
     @Test
-    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOn()
+    public void testIntercept_locked_communalOn_appNotEnabled_showWhenLockedOn_allowlistEnabled()
             throws RemoteException {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         mBinder.setCommunalViewShowing(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
+                UserHandle.SYSTEM)).thenReturn(true);
         mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;
 
         allowPackages("package1,package2");
-        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesIntercept();
     }
 
     @Test
-    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOff()
+    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOff_allowlistEnabled()
             throws RemoteException {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         mBinder.setCommunalViewShowing(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
+                UserHandle.SYSTEM)).thenReturn(true);
         mAInfo.flags = 0;
 
         allowPackages(TEST_PACKAGE_NAME);
-        // TODO(b/191994709): Fix this assertion once we properly intercept activities.
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        // TODO(b/191994709): Fix this assertion once we start checking showWhenLocked
+        assertDoesNotIntercept();
     }
 
     @Test
-    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn()
+    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn_allowlistEnabled()
             throws RemoteException {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
         mBinder.setCommunalViewShowing(true);
         when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
+                UserHandle.SYSTEM)).thenReturn(true);
+
         mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;
 
         allowPackages(TEST_PACKAGE_NAME);
-        assertThat(mActivityInterceptorCallback.intercept(buildActivityInfo(intent))).isNull();
+        assertDoesNotIntercept();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_appEnabled_showWhenLockedOn_allowlistDisabled()
+            throws RemoteException {
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(CompatChanges.isChangeEnabled(ALLOW_COMMUNAL_MODE_WITH_USER_CONSENT, TEST_PACKAGE_NAME,
+                UserHandle.SYSTEM)).thenReturn(false);
+
+        mAInfo.flags = FLAG_SHOW_WHEN_LOCKED;
+
+        allowPackages(TEST_PACKAGE_NAME);
+        assertDoesIntercept();
+    }
+
+    @Test
+    public void testIntercept_locked_communalOn_dream() throws RemoteException {
+        mBinder.setCommunalViewShowing(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+        when(mDreamManagerInternal.getActiveDreamComponent(false)).thenReturn(
+                new ComponentName(TEST_PACKAGE_NAME, "SomeClass"));
+
+        allowPackages(TEST_PACKAGE_NAME);
+        assertDoesNotIntercept();
+    }
+
+    @Test
+    public void testUpdateSettings_packageUninstalled() {
+        allowPackages("package1,package2");
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+
+        mPackageReceiver.onReceive(mContextSpy,
+                createPackageIntent("package1", ACTION_PACKAGE_REMOVED));
+
+        assertThat(getAllowedPackages()).isEqualTo("package2");
+    }
+
+    @Test
+    public void testUpdateSettings_nullAction_doesNothing() {
+        allowPackages("package1,package2");
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+
+        mPackageReceiver.onReceive(mContextSpy,
+                createPackageIntent("package1", null));
+
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+    }
+
+    @Test
+    public void testUpdateSettings_invalidPackage_doesNothing() {
+        allowPackages("package1,package2");
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+
+        mPackageReceiver.onReceive(mContextSpy,
+                createPackageIntent("package3", ACTION_PACKAGE_REMOVED));
+
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+    }
+
+    @Test
+    public void testUpdateSettings_onBoot() {
+        allowPackages("package1,package2");
+        assertThat(getAllowedPackages()).isEqualTo("package1,package2");
+
+        when(CommunalManagerService.isPackageInstalled(eq("package1"), any())).thenReturn(true);
+        when(CommunalManagerService.isPackageInstalled(eq("package2"), any())).thenReturn(false);
+
+        mService.onBootPhase(SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
+
+        assertThat(getAllowedPackages()).isEqualTo("package1");
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/communal/OWNERS b/services/tests/mockingservicestests/src/com/android/server/communal/OWNERS
new file mode 100644
index 0000000..9ec6665
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/communal/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/communal/OWNERS
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceHibernationTests.kt b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceHibernationTests.kt
index 084df5a35..dcdbb09 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceHibernationTests.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerServiceHibernationTests.kt
@@ -16,8 +16,10 @@
 
 package com.android.server.pm
 
+import android.content.Context
 import android.os.Build
 import android.os.Handler
+import android.os.PowerManager
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION
 import android.testing.AndroidTestingRunner
@@ -56,6 +58,8 @@
 
     @Mock
     lateinit var appHibernationManager: AppHibernationManagerInternal
+    @Mock
+    lateinit var powerManager: PowerManager
 
     @Before
     @Throws(Exception::class)
@@ -69,6 +73,24 @@
             .thenReturn(appHibernationManager)
         whenever(rule.mocks().injector.handler)
             .thenReturn(Handler(TestableLooper.get(this).looper))
+        val injector = object : PackageDexOptimizer.Injector {
+            override fun getAppHibernationManagerInternal(): AppHibernationManagerInternal {
+                return appHibernationManager
+            }
+
+            override fun getPowerManager(context: Context?): PowerManager {
+                return powerManager
+            }
+        }
+        val packageDexOptimizer = PackageDexOptimizer(
+            injector,
+            rule.mocks().installer,
+            rule.mocks().installLock,
+            rule.mocks().context,
+            "*dexopt*")
+        whenever(rule.mocks().injector.packageDexOptimizer)
+            .thenReturn(packageDexOptimizer)
+        whenever(appHibernationManager.isOatArtifactDeletionEnabled).thenReturn(true)
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 0cd6d86..0ac00aa 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -19,13 +19,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.biometrics.common.CommonProps;
 import android.hardware.biometrics.face.IFace;
+import android.hardware.biometrics.face.ISession;
 import android.hardware.biometrics.face.SensorProps;
+import android.os.RemoteException;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 
@@ -33,7 +37,6 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
-import com.android.server.biometrics.sensors.BaseClientMonitor;
 import com.android.server.biometrics.sensors.BiometricScheduler;
 import com.android.server.biometrics.sensors.HalClientMonitor;
 import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -55,6 +58,8 @@
     private Context mContext;
     @Mock
     private UserManager mUserManager;
+    @Mock
+    private IFace mDaemon;
 
     private SensorProps[] mSensorProps;
     private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -65,11 +70,12 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
         when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
         when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
+        when(mDaemon.createSession(anyInt(), anyInt(), any())).thenReturn(mock(ISession.class));
 
         final SensorProps sensor1 = new SensorProps();
         sensor1.commonProps = new CommonProps();
@@ -78,11 +84,11 @@
         sensor2.commonProps = new CommonProps();
         sensor2.commonProps.sensorId = 1;
 
-        mSensorProps = new SensorProps[] {sensor1, sensor2};
+        mSensorProps = new SensorProps[]{sensor1, sensor2};
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFaceProvider = new TestableFaceProvider(mContext, mSensorProps, TAG,
+        mFaceProvider = new TestableFaceProvider(mDaemon, mContext, mSensorProps, TAG,
                 mLockoutResetDispatcher);
     }
 
@@ -127,17 +133,20 @@
     }
 
     private static class TestableFaceProvider extends FaceProvider {
-        public TestableFaceProvider(@NonNull Context context,
+        private final IFace mDaemon;
+
+        TestableFaceProvider(@NonNull IFace daemon,
+                @NonNull Context context,
                 @NonNull SensorProps[] props,
                 @NonNull String halInstanceName,
                 @NonNull LockoutResetDispatcher lockoutResetDispatcher) {
             super(context, props, halInstanceName, lockoutResetDispatcher);
+            mDaemon = daemon;
         }
 
         @Override
         synchronized IFace getHalInstance() {
-            return mock(IFace.class);
+            return mDaemon;
         }
     }
-
 }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
index 8b7c90d..73f1516 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProviderTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -28,8 +29,10 @@
 import android.content.res.TypedArray;
 import android.hardware.biometrics.common.CommonProps;
 import android.hardware.biometrics.fingerprint.IFingerprint;
+import android.hardware.biometrics.fingerprint.ISession;
 import android.hardware.biometrics.fingerprint.SensorLocation;
 import android.hardware.biometrics.fingerprint.SensorProps;
+import android.os.RemoteException;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
 
@@ -63,6 +66,8 @@
     @Mock
     private UserManager mUserManager;
     @Mock
+    private IFingerprint mDaemon;
+    @Mock
     private GestureAvailabilityDispatcher mGestureAvailabilityDispatcher;
     @Mock
     private FingerprintStateCallback mFingerprintStateCallback;
@@ -76,13 +81,14 @@
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
 
         when(mContext.getResources()).thenReturn(mResources);
         when(mResources.obtainTypedArray(anyInt())).thenReturn(mock(TypedArray.class));
         when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
         when(mUserManager.getAliveUsers()).thenReturn(new ArrayList<>());
+        when(mDaemon.createSession(anyInt(), anyInt(), any())).thenReturn(mock(ISession.class));
 
         final SensorProps sensor1 = new SensorProps();
         sensor1.commonProps = new CommonProps();
@@ -97,8 +103,9 @@
 
         mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
 
-        mFingerprintProvider = new TestableFingerprintProvider(mContext, mFingerprintStateCallback,
-                mSensorProps, TAG, mLockoutResetDispatcher, mGestureAvailabilityDispatcher);
+        mFingerprintProvider = new TestableFingerprintProvider(mDaemon, mContext,
+                mFingerprintStateCallback, mSensorProps, TAG, mLockoutResetDispatcher,
+                mGestureAvailabilityDispatcher);
     }
 
     @SuppressWarnings("rawtypes")
@@ -142,7 +149,10 @@
     }
 
     private static class TestableFingerprintProvider extends FingerprintProvider {
-        public TestableFingerprintProvider(@NonNull Context context,
+        private final IFingerprint mDaemon;
+
+        TestableFingerprintProvider(@NonNull IFingerprint daemon,
+                @NonNull Context context,
                 @NonNull FingerprintStateCallback fingerprintStateCallback,
                 @NonNull SensorProps[] props,
                 @NonNull String halInstanceName,
@@ -150,11 +160,12 @@
                 @NonNull GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
             super(context, fingerprintStateCallback, props, halInstanceName, lockoutResetDispatcher,
                     gestureAvailabilityDispatcher);
+            mDaemon = daemon;
         }
 
         @Override
         synchronized IFingerprint getHalInstance() {
-            return mock(IFingerprint.class);
+            return mDaemon;
         }
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
index d10419d..d4b1165 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java
@@ -439,6 +439,12 @@
     }
 
     @Override
+    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) {
+        mMockSystemServices.registerReceiver(receiver, filter, null);
+        return spiedContext.registerReceiver(receiver, filter, flags);
+    }
+
+    @Override
     public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
             String broadcastPermission, Handler scheduler) {
         mMockSystemServices.registerReceiver(receiver, filter, scheduler);
diff --git a/services/tests/servicestests/src/com/android/server/health/OWNERS b/services/tests/servicestests/src/com/android/server/health/OWNERS
new file mode 100644
index 0000000..81522fc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/health/OWNERS
@@ -0,0 +1 @@
+file:platform/hardware/interfaces:/health/aidl/OWNERS
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 fb8bc7b..c0959d3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -419,6 +419,7 @@
         task.getBounds(taskBounds);
         taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom);
         spyOn(taskFragment);
+        mockSurfaceFreezerSnapshot(taskFragment.mSurfaceFreezer);
 
         assertTrue(mDc.mChangingContainers.isEmpty());
         assertFalse(mDc.mAppTransition.isTransitionSet());
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
index 9d2a691..b4c449a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationControllerTest.java
@@ -124,7 +124,7 @@
         // Verify that the finish callback to reparent the leash is called
         verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_RECENTS), eq(adapter));
         // Verify the animation canceled callback to the app was made
-        verify(mMockRunner).onAnimationCanceled(null /* taskSnapshot */);
+        verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
         verifyNoMoreInteractionsExceptAsBinder(mMockRunner);
     }
 
@@ -207,7 +207,8 @@
         wallpaperWindowToken.cancelAnimation();
         assertTrue(mController.isAnimatingTask(activity.getTask()));
         assertFalse(mController.isAnimatingWallpaper(wallpaperWindowToken));
-        verify(mMockRunner, never()).onAnimationCanceled(null /* taskSnapshot */);
+        verify(mMockRunner, never()).onAnimationCanceled(null /* taskIds */,
+                null /* taskSnapshots */);
     }
 
     @Test
@@ -254,7 +255,7 @@
 
         mController.setDeferredCancel(true /* deferred */, false /* screenshot */);
         mController.cancelAnimationWithScreenshot(false /* screenshot */);
-        verify(mMockRunner).onAnimationCanceled(null /* taskSnapshot */);
+        verify(mMockRunner).onAnimationCanceled(null /* taskIds */, null /* taskSnapshots */);
 
         // Simulate the app transition finishing
         mController.mAppTransitionListener.onAppTransitionStartingLocked(false, false, 0, 0, 0);
@@ -281,7 +282,8 @@
                 anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
         mController.setDeferredCancel(true /* deferred */, true /* screenshot */);
         mController.cancelAnimationWithScreenshot(true /* screenshot */);
-        verify(mMockRunner).onAnimationCanceled(mMockTaskSnapshot /* taskSnapshot */);
+        verify(mMockRunner).onAnimationCanceled(any(int[].class) /* taskIds */,
+                any(TaskSnapshot[].class) /* taskSnapshots */);
 
         // Continue the animation (simulating a call to cleanupScreenshot())
         mController.continueDeferredCancelAnimation();
@@ -322,7 +324,7 @@
         doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(),
                 anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
         mController.cancelAnimationWithScreenshot(true /* screenshot */);
-        verify(mMockRunner).onAnimationCanceled(any());
+        verify(mMockRunner).onAnimationCanceled(any(), any());
 
         // Simulate process crashing and ensure the animation is still canceled
         mController.binderDied();
@@ -700,7 +702,7 @@
         mController.setWillFinishToHome(true);
         mController.cancelAnimationForDisplayChange();
 
-        verify(mMockRunner).onAnimationCanceled(any());
+        verify(mMockRunner).onAnimationCanceled(any(), any());
         verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_TOP, false);
     }
 
@@ -715,7 +717,7 @@
         mController.setWillFinishToHome(false);
         mController.cancelAnimationForDisplayChange();
 
-        verify(mMockRunner).onAnimationCanceled(any());
+        verify(mMockRunner).onAnimationCanceled(any(), any());
         verify(mAnimationCallbacks).onAnimationFinished(REORDER_MOVE_TO_ORIGINAL_POSITION, false);
     }
 
@@ -735,7 +737,7 @@
         doReturn(mMockTaskSnapshot).when(mWm.mTaskSnapshotController).getSnapshot(anyInt(),
                 anyInt(), eq(false) /* restoreFromDisk */, eq(false) /* isLowResolution */);
         mController.cancelAnimationForHomeStart();
-        verify(mMockRunner).onAnimationCanceled(any());
+        verify(mMockRunner).onAnimationCanceled(any(), any());
 
         // Continue the animation (simulating a call to cleanupScreenshot())
         mController.continueDeferredCancelAnimation();
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 cb209abf..42f4d58 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -82,6 +82,7 @@
 
     @Test
     public void testStartChangeTransition_resetSurface() {
+        mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
         final Rect startBounds = new Rect(0, 0, 1000, 1000);
         final Rect endBounds = new Rect(500, 500, 1000, 1000);
         mTaskFragment.setBounds(startBounds);
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 db7def8..bec53d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -1119,16 +1119,15 @@
         final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         spyOn(container);
         spyOn(surfaceAnimator);
-        spyOn(surfaceFreezer);
+        mockSurfaceFreezerSnapshot(surfaceFreezer);
         doReturn(t).when(container).getPendingTransaction();
         doReturn(t).when(container).getSyncTransaction();
 
         // Leash and snapshot created for change transition.
         container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-        // Can't really take a snapshot, manually set one.
-        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
 
         assertNotNull(surfaceFreezer.mLeash);
+        assertNotNull(surfaceFreezer.mSnapshot);
         assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
 
         // Start animation: surfaceAnimator take over the leash and snapshot from surfaceFreezer.
@@ -1145,9 +1144,9 @@
 
         // Prepare another change transition.
         container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-        surfaceFreezer.mSnapshot = mock(SurfaceFreezer.Snapshot.class);
 
         assertNotNull(surfaceFreezer.mLeash);
+        assertNotNull(surfaceFreezer.mSnapshot);
         assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
         assertNotEquals(prevLeash, container.getAnimationLeash());
 
@@ -1174,6 +1173,64 @@
         assertNull(surfaceAnimator.mSnapshot);
     }
 
+    @Test
+    public void testUnfreezeWindow_removeWindowFromChanging() {
+        final WindowContainer container = createTaskFragmentWithParentTask(
+                createTask(mDisplayContent), false);
+        mockSurfaceFreezerSnapshot(container.mSurfaceFreezer);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+
+        assertTrue(mDisplayContent.mChangingContainers.contains(container));
+
+        container.mSurfaceFreezer.unfreeze(t);
+
+        assertFalse(mDisplayContent.mChangingContainers.contains(container));
+    }
+
+    @Test
+    public void testFailToTaskSnapshot_unfreezeWindow() {
+        final WindowContainer container = createTaskFragmentWithParentTask(
+                createTask(mDisplayContent), false);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        spyOn(container.mSurfaceFreezer);
+
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+
+        verify(container.mSurfaceFreezer).freeze(any(), any(), any(), any());
+        verify(container.mSurfaceFreezer).unfreeze(any());
+        assertTrue(mDisplayContent.mChangingContainers.isEmpty());
+    }
+
+    @Test
+    public void testRemoveUnstartedFreezeSurfaceWhenFreezeAgain() {
+        final WindowContainer container = createTaskFragmentWithParentTask(
+                createTask(mDisplayContent), false);
+        container.mSurfaceControl = mock(SurfaceControl.class);
+        final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
+        mockSurfaceFreezerSnapshot(surfaceFreezer);
+        final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
+        spyOn(container);
+        doReturn(t).when(container).getPendingTransaction();
+        doReturn(t).when(container).getSyncTransaction();
+
+        // Leash and snapshot created for change transition.
+        container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
+
+        assertNotNull(surfaceFreezer.mLeash);
+        assertNotNull(surfaceFreezer.mSnapshot);
+
+        final SurfaceControl prevLeash = surfaceFreezer.mLeash;
+        final SurfaceFreezer.Snapshot prevSnapshot = surfaceFreezer.mSnapshot;
+        spyOn(prevSnapshot);
+
+        container.initializeChangeTransition(new Rect(0, 0, 1500, 2500));
+
+        verify(t).remove(prevLeash);
+        verify(prevSnapshot).destroy(t);
+    }
+
     /* Used so we can gain access to some protected members of the {@link WindowContainer} class */
     private static class TestWindowContainer extends WindowContainer<TestWindowContainer> {
         private final int mLayer;
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index b175974..386ff4b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -75,6 +75,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
 import android.os.Build;
 import android.os.Bundle;
@@ -869,6 +870,21 @@
         mAtm.mRootWindowContainer.mDefaultMinSizeOfResizeableTaskDp = 1;
     }
 
+    /** Mocks the behavior of taking a snapshot. */
+    void mockSurfaceFreezerSnapshot(SurfaceFreezer surfaceFreezer) {
+        final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
+                mock(SurfaceControl.ScreenshotHardwareBuffer.class);
+        final HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
+        spyOn(surfaceFreezer);
+        doReturn(screenshotBuffer).when(surfaceFreezer)
+                .createSnapshotBufferInner(any(), any());
+        doReturn(null).when(surfaceFreezer)
+                .createFromHardwareBufferInner(any());
+        doReturn(hardwareBuffer).when(screenshotBuffer).getHardwareBuffer();
+        doReturn(100).when(hardwareBuffer).getWidth();
+        doReturn(100).when(hardwareBuffer).getHeight();
+    }
+
     /**
      * Builder for creating new activities.
      */
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index b3b1491..0bd7b20 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -94,7 +94,8 @@
     private static final String PROP_DISABLE_QUOTA = "fw.disable_quota";
     private static final String PROP_VERIFY_STORAGE = "fw.verify_storage";
 
-    private static final long DELAY_IN_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
+    private static final long DELAY_CHECK_STORAGE_DELTA = 30 * DateUtils.SECOND_IN_MILLIS;
+    private static final long DELAY_RECALCULATE_QUOTAS = 10 * DateUtils.HOUR_IN_MILLIS;
     private static final long DEFAULT_QUOTA = DataUnit.MEBIBYTES.toBytes(64);
 
     public static class Lifecycle extends SystemService {
@@ -529,6 +530,7 @@
     private class H extends Handler {
         private static final int MSG_CHECK_STORAGE_DELTA = 100;
         private static final int MSG_LOAD_CACHED_QUOTAS_FROM_FILE = 101;
+        private static final int MSG_RECALCULATE_QUOTAS = 102;
         /**
          * By only triggering a re-calculation after the storage has changed sizes, we can avoid
          * recalculating quotas too often. Minimum change delta defines the percentage of change
@@ -568,7 +570,7 @@
                         recalculateQuotas(getInitializedStrategy());
                         notifySignificantDelta();
                     }
-                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
+                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_CHECK_STORAGE_DELTA);
                     break;
                 }
                 case MSG_LOAD_CACHED_QUOTAS_FROM_FILE: {
@@ -588,7 +590,13 @@
                         mPreviousBytes = mStats.getAvailableBytes();
                         recalculateQuotas(strategy);
                     }
-                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_IN_MILLIS);
+                    sendEmptyMessageDelayed(MSG_CHECK_STORAGE_DELTA, DELAY_CHECK_STORAGE_DELTA);
+                    sendEmptyMessageDelayed(MSG_RECALCULATE_QUOTAS, DELAY_RECALCULATE_QUOTAS);
+                    break;
+                }
+                case MSG_RECALCULATE_QUOTAS: {
+                    recalculateQuotas(getInitializedStrategy());
+                    sendEmptyMessageDelayed(MSG_RECALCULATE_QUOTAS, DELAY_RECALCULATE_QUOTAS);
                     break;
                 }
                 default:
diff --git a/telephony/java/android/telephony/PhysicalChannelConfig.java b/telephony/java/android/telephony/PhysicalChannelConfig.java
index d250088..95448c7 100644
--- a/telephony/java/android/telephony/PhysicalChannelConfig.java
+++ b/telephony/java/android/telephony/PhysicalChannelConfig.java
@@ -242,6 +242,11 @@
     }
 
     /**
+     * The physical cell ID which differentiates cells using the same radio channel.
+     *
+     * In GERAN, this value is the BSIC. The range is [0-63].
+     * Reference: 3GPP TS 3.03 section 4.2.2.
+     *
      * In UTRAN, this value is primary scrambling code. The range is [0, 511].
      * Reference: 3GPP TS 25.213 section 5.2.2.
      *
diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml
index 269c575..22fe424 100644
--- a/tests/HwAccelerationTest/AndroidManifest.xml
+++ b/tests/HwAccelerationTest/AndroidManifest.xml
@@ -437,7 +437,7 @@
         </activity>
 
         <activity android:name=".PenStylusActivity"
-                  android:label="Pen (BUGGED)/Draw"
+                  android:label="Pen/Draw"
                   android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/tests/HwAccelerationTest/jni/native-lib.cpp b/tests/HwAccelerationTest/jni/native-lib.cpp
index d6b1c49..407d4bf 100644
--- a/tests/HwAccelerationTest/jni/native-lib.cpp
+++ b/tests/HwAccelerationTest/jni/native-lib.cpp
@@ -41,7 +41,7 @@
 };
 
 extern "C" JNIEXPORT jlong JNICALL
-Java_com_android_test_hwui_FrontBufferedLayer_nCreate(JNIEnv* env, jobject jSurface) {
+Java_com_android_test_hwui_FrontBufferedLayer_nCreate(JNIEnv* env, jclass, jobject jSurface) {
     ANativeWindow* window = ANativeWindow_fromSurface(env, jSurface);
     MyWrapper* wrapper = new MyWrapper(window);
     ANativeWindow_release(window);
@@ -49,13 +49,13 @@
 }
 
 extern "C" JNIEXPORT void JNICALL
-Java_com_android_test_hwui_FrontBufferedLayer_nDestroy(jlong ptr) {
+Java_com_android_test_hwui_FrontBufferedLayer_nDestroy(JNIEnv*, jclass, jlong ptr) {
     MyWrapper* wrapper = reinterpret_cast<MyWrapper*>(ptr);
     delete wrapper;
 }
 
 extern "C" JNIEXPORT void JNICALL Java_com_android_test_hwui_FrontBufferedLayer_nUpdateBuffer(
-        JNIEnv* env, jlong ptr, jobject jbuffer) {
+        JNIEnv* env, jclass, jlong ptr, jobject jbuffer) {
     MyWrapper* wrapper = reinterpret_cast<MyWrapper*>(ptr);
     AHardwareBuffer* buffer = AHardwareBuffer_fromHardwareBuffer(env, jbuffer);
     wrapper->setBuffer(buffer);
diff --git a/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java b/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
index c4967eb..4083bea 100644
--- a/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
+++ b/wifi/tests/src/android/net/wifi/WifiNetworkScoreCacheTest.java
@@ -76,7 +76,7 @@
 
     private static ScanResult buildScanResult(String ssid, String bssid) {
         return new ScanResult(
-                WifiSsid.createFromAsciiEncoded(ssid),
+                WifiSsid.fromUtf8Text(ssid),
                 bssid,
                 "" /* caps */,
                 0 /* level */,