Merge "Automatic reformatting by ktlint.py" into tm-qpr-dev
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java
new file mode 100644
index 0000000..7ed97fb
--- /dev/null
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.multiuser;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+
+public class BroadcastWaiter implements Closeable {
+    private final Context mContext;
+    private final String mTag;
+    private final int mTimeoutInSecond;
+    private final Set<String> mActions;
+
+    private final Set<String> mActionReceivedForUser = new HashSet<>();
+    private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>();
+
+    private final Map<String, Semaphore> mSemaphoresMap = new ConcurrentHashMap<>();
+    private Semaphore getSemaphore(final String action, final int userId) {
+        final String key = action + userId;
+        return mSemaphoresMap.computeIfAbsent(key, (String absentKey) -> new Semaphore(0));
+    }
+
+    public BroadcastWaiter(Context context, String tag, int timeoutInSecond, String... actions) {
+        mContext = context;
+        mTag = tag + "_" + BroadcastWaiter.class.getSimpleName();
+        mTimeoutInSecond = timeoutInSecond;
+
+        mActions = new HashSet<>(Arrays.asList(actions));
+        mActions.forEach(this::registerBroadcastReceiver);
+    }
+
+    private void registerBroadcastReceiver(String action) {
+        Log.d(mTag, "#registerBroadcastReceiver for " + action);
+
+        final IntentFilter filter = new IntentFilter(action);
+        if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+            filter.addDataScheme(ContentResolver.SCHEME_FILE);
+        }
+
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (action.equals(intent.getAction())) {
+                    final int userId = getSendingUserId();
+                    final String data = intent.getDataString();
+                    Log.d(mTag, "Received " + action + " for user " + userId
+                            + (!TextUtils.isEmpty(data) ? " with " + data : ""));
+                    mActionReceivedForUser.add(action + userId);
+                    getSemaphore(action, userId).release();
+                }
+            }
+        };
+
+        mContext.registerReceiverForAllUsers(receiver, filter, null, null);
+        mBroadcastReceivers.add(receiver);
+    }
+
+    @Override
+    public void close() {
+        mBroadcastReceivers.forEach(mContext::unregisterReceiver);
+    }
+
+    public boolean hasActionBeenReceivedForUser(String action, int userId) {
+        return mActionReceivedForUser.contains(action + userId);
+    }
+
+    public boolean waitActionForUser(String action, int userId) {
+        Log.d(mTag, "#waitActionForUser(action: " + action + ", userId: " + userId + ")");
+
+        if (!mActions.contains(action)) {
+            Log.d(mTag, "No broadcast receivers registered for " + action);
+            return false;
+        }
+
+        try {
+            if (!getSemaphore(action, userId).tryAcquire(1, mTimeoutInSecond, SECONDS)) {
+                Log.e(mTag, action + " broadcast wasn't received for user " + userId);
+                return false;
+            }
+        } catch (InterruptedException e) {
+            Log.e(mTag, "Interrupted while waiting " + action + " for user " + userId);
+            return false;
+        }
+        return true;
+    }
+
+    public boolean waitActionForUserIfNotReceivedYet(String action, int userId) {
+        return hasActionBeenReceivedForUser(action, userId)
+                || waitActionForUser(action, userId);
+    }
+}
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index b6f2152..a44d939 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -26,7 +26,6 @@
 import android.app.UserSwitchObserver;
 import android.app.WaitResult;
 import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.IIntentSender;
@@ -115,6 +114,7 @@
     private PackageManager mPm;
     private ArrayList<Integer> mUsersToRemove;
     private boolean mHasManagedUserFeature;
+    private BroadcastWaiter mBroadcastWaiter;
 
     private final BenchmarkRunner mRunner = new BenchmarkRunner();
     @Rule
@@ -129,6 +129,10 @@
         mUsersToRemove = new ArrayList<>();
         mPm = context.getPackageManager();
         mHasManagedUserFeature = mPm.hasSystemFeature(PackageManager.FEATURE_MANAGED_USERS);
+        mBroadcastWaiter = new BroadcastWaiter(context, TAG, TIMEOUT_IN_SECOND,
+                Intent.ACTION_USER_STARTED,
+                Intent.ACTION_MEDIA_MOUNTED,
+                Intent.ACTION_USER_UNLOCKED);
         removeAnyPreviousTestUsers();
         if (mAm.getCurrentUser() != UserHandle.USER_SYSTEM) {
             Log.w(TAG, "WARNING: Tests are being run from user " + mAm.getCurrentUser()
@@ -138,6 +142,7 @@
 
     @After
     public void tearDown() {
+        mBroadcastWaiter.close();
         for (int userId : mUsersToRemove) {
             try {
                 mUm.removeUser(userId);
@@ -168,12 +173,10 @@
             Log.i(TAG, "Starting timer");
             final int userId = createUserNoFlags();
 
-            final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId);
             // Don't use this.startUserInBackgroundAndWaitForUnlock() since only waiting until
             // ACTION_USER_STARTED.
             mIam.startUserInBackground(userId);
-            waitForLatch("Failed to achieve ACTION_USER_STARTED for user " + userId, latch);
+            waitForBroadcast(Intent.ACTION_USER_STARTED, userId);
 
             mRunner.pauseTiming();
             Log.i(TAG, "Stopping timer");
@@ -191,13 +194,11 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int userId = createUserNoFlags();
-            final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId);
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
             mIam.startUserInBackground(userId);
-            waitForLatch("Failed to achieve ACTION_USER_STARTED for user " + userId, latch);
+            waitForBroadcast(Intent.ACTION_USER_STARTED, userId);
 
             mRunner.pauseTiming();
             Log.i(TAG, "Stopping timer");
@@ -255,14 +256,11 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
-            final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch, testUser);
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
             mAm.switchUser(testUser);
-            waitForLatch("Failed to achieve 2nd ACTION_USER_UNLOCKED for user " + testUser, latch);
-
+            waitForBroadcast(Intent.ACTION_USER_UNLOCKED, testUser);
 
             mRunner.pauseTiming();
             Log.i(TAG, "Stopping timer");
@@ -298,13 +296,11 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int userId = createUserNoFlags();
-            final CountDownLatch latch1 = new CountDownLatch(1);
-            final CountDownLatch latch2 = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch1, userId);
-            registerMediaBroadcastReceiver(latch2, userId);
             mIam.startUserInBackground(userId);
-            waitForLatch("Failed to achieve ACTION_USER_STARTED for user " + userId, latch1);
-            waitForLatch("Failed to achieve ACTION_MEDIA_MOUNTED for user " + userId, latch2);
+
+            waitForBroadcast(Intent.ACTION_USER_STARTED, userId);
+            waitForBroadcast(Intent.ACTION_MEDIA_MOUNTED, userId);
+
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -347,10 +343,9 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int userId = createUserWithFlags(UserInfo.FLAG_EPHEMERAL | UserInfo.FLAG_DEMO);
-            final CountDownLatch prelatch = new CountDownLatch(1);
-            registerMediaBroadcastReceiver(prelatch, userId);
             switchUser(userId);
-            waitForLatch("Failed to achieve ACTION_MEDIA_MOUNTED for user " + userId, prelatch);
+            waitForBroadcast(Intent.ACTION_MEDIA_MOUNTED, userId);
+
             final CountDownLatch latch = new CountDownLatch(1);
             InstrumentationRegistry.getContext().registerReceiver(new BroadcastReceiver() {
                 @Override
@@ -552,10 +547,9 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int userId = createManagedProfile();
-            final CountDownLatch prelatch = new CountDownLatch(1);
-            registerMediaBroadcastReceiver(prelatch, userId);
             startUserInBackgroundAndWaitForUnlock(userId);
-            waitForLatch("Failed to achieve ACTION_MEDIA_MOUNTED for user " + userId, prelatch);
+            waitForBroadcast(Intent.ACTION_MEDIA_MOUNTED, userId);
+
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -710,13 +704,9 @@
         final int origUser = mAm.getCurrentUser();
         // First, create and switch to testUser, waiting for its ACTION_USER_UNLOCKED
         final int testUser = createUserNoFlags();
-        final CountDownLatch latch1 = new CountDownLatch(1);
-        final CountDownLatch latch2 = new CountDownLatch(1);
-        registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch1, testUser);
-        registerMediaBroadcastReceiver(latch2, testUser);
         mAm.switchUser(testUser);
-        waitForLatch("Failed to achieve initial ACTION_USER_UNLOCKED for user " + testUser, latch1);
-        waitForLatch("Failed to achieve initial ACTION_MEDIA_MOUNTED for user " + testUser, latch2);
+        waitForBroadcast(Intent.ACTION_USER_UNLOCKED, testUser);
+        waitForBroadcast(Intent.ACTION_MEDIA_MOUNTED, testUser);
 
         // Second, switch back to origUser, waiting merely for switchUser() to finish
         switchUser(origUser);
@@ -786,50 +776,6 @@
                 }, TAG);
     }
 
-    private void registerBroadcastReceiver(final String action, final CountDownLatch latch,
-            final int userId) {
-        InstrumentationRegistry.getContext().registerReceiverAsUser(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (action.equals(intent.getAction()) && intent.getIntExtra(
-                        Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == userId) {
-                    latch.countDown();
-                }
-            }
-        }, UserHandle.of(userId), new IntentFilter(action), null, null);
-    }
-
-    /**
-     * Register for a broadcast to indicate that Storage has processed the given user.
-     * Without this as part of setup, for tests dealing with already-switched users, Storage may not
-     * have finished, making the resulting processing inconsistent.
-     *
-     * Strictly speaking, the receiver should always be unregistered afterwards, but we don't
-     * necessarily bother since receivers from failed tests will be removed on test uninstallation.
-     */
-    private void registerMediaBroadcastReceiver(final CountDownLatch latch, final int userId) {
-        final String action = Intent.ACTION_MEDIA_MOUNTED;
-
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(action);
-        filter.addDataScheme(ContentResolver.SCHEME_FILE);
-
-        final Context context = InstrumentationRegistry.getContext();
-        context.registerReceiverAsUser(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                final String data = intent.getDataString();
-                if (action.equals(intent.getAction())) {
-                    Log.d(TAG, "Received ACTION_MEDIA_MOUNTED with " + data);
-                    if (data != null && data.contains("/" + userId)) {
-                        latch.countDown();
-                        context.unregisterReceiver(this);
-                    }
-                }
-            }
-        }, UserHandle.of(userId), filter, null, null);
-    }
-
     private class ProgressWaiter extends IProgressListener.Stub {
         private final CountDownLatch mFinishedLatch = new CountDownLatch(1);
 
@@ -854,6 +800,17 @@
         }
     }
 
+    /**
+     * Waits TIMEOUT_IN_SECOND for the broadcast to be received, otherwise declares the given error.
+     * It only works for the broadcasts provided in {@link #mBroadcastWaiter}'s instantiation above.
+     * @param action action of the broadcast, i.e. {@link Intent#ACTION_USER_STARTED}
+     * @param userId sendingUserId of the broadcast. See {@link BroadcastReceiver#getSendingUserId}
+     */
+    private void waitForBroadcast(String action, int userId) {
+        attestTrue("Failed to achieve " + action + " for user " + userId,
+                mBroadcastWaiter.waitActionForUser(action, userId));
+    }
+
     /** Waits TIMEOUT_IN_SECOND for the latch to complete, otherwise declares the given error. */
     private void waitForLatch(String errMsg, CountDownLatch latch) {
         boolean success = false;
@@ -880,6 +837,9 @@
     }
 
     private void removeUser(int userId) {
+        if (mBroadcastWaiter.hasActionBeenReceivedForUser(Intent.ACTION_USER_STARTED, userId)) {
+            mBroadcastWaiter.waitActionForUserIfNotReceivedYet(Intent.ACTION_MEDIA_MOUNTED, userId);
+        }
         try {
             mUm.removeUser(userId);
             final long startTime = System.currentTimeMillis();
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 7141259..90c37d1 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -6433,6 +6433,20 @@
     }
 
     /**
+     * Ensures the activity's result is immediately returned to the caller when {@link #finish()}
+     * is invoked
+     *
+     * <p>Should be invoked alongside {@link #setResult(int, Intent)}, so the provided results are
+     * in place before finishing. Must only be invoked during MediaProjection setup.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
+    public final void setForceSendResultForMediaProjection() {
+        ActivityClient.getInstance().setForceSendResultForMediaProjection(mToken);
+    }
+
+    /**
      * Call this to set the result that your activity will return to its
      * caller.
      *
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 73678d9..1d3b5e2 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -17,6 +17,7 @@
 package android.app;
 
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -184,6 +185,15 @@
         }
     }
 
+    @RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
+    void setForceSendResultForMediaProjection(IBinder token) {
+        try {
+            getActivityClientController().setForceSendResultForMediaProjection(token);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     public boolean isTopOfTask(IBinder token) {
         try {
             return getActivityClientController().isTopOfTask(token);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 0138186..7aeb2b2 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -67,6 +67,12 @@
     boolean finishActivityAffinity(in IBinder token);
     /** Finish all activities that were started for result from the specified activity. */
     void finishSubActivity(in IBinder token, in String resultWho, int requestCode);
+    /**
+     * Indicates that when the activity finsihes, the result should be immediately sent to the
+     * originating activity. Must only be invoked during MediaProjection setup.
+     */
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)")
+    void setForceSendResultForMediaProjection(in IBinder token);
 
     boolean isTopOfTask(in IBinder token);
     boolean willActivityBeVisible(in IBinder token);
diff --git a/core/res/res/values-land/dimens.xml b/core/res/res/values-land/dimens.xml
index ca549ae..f58c4b0 100644
--- a/core/res/res/values-land/dimens.xml
+++ b/core/res/res/values-land/dimens.xml
@@ -74,7 +74,7 @@
      <!-- Floating toolbar dimensions -->
      <dimen name="floating_toolbar_preferred_width">544dp</dimen>
 
-    <dimen name="chooser_preview_width">480dp</dimen>
+    <dimen name="chooser_preview_width">412dp</dimen>
 
     <dimen name="toast_y_offset">24dp</dimen>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
index bebafb9..992f315 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java
@@ -27,7 +27,6 @@
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformTaskListener;
 import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
 import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
@@ -35,6 +34,7 @@
 import com.android.wm.shell.startingsurface.StartingWindowController;
 import com.android.wm.shell.transition.DefaultMixedHandler;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 
 import java.util.Optional;
@@ -55,7 +55,7 @@
     private final Optional<SplitScreenController> mSplitScreenOptional;
     private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
     private final FullscreenTaskListener mFullscreenTaskListener;
-    private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
+    private final Optional<UnfoldAnimationController> mUnfoldController;
     private final Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler;
     private final Optional<FreeformTaskListener<?>> mFreeformTaskListenerOptional;
     private final ShellExecutor mMainExecutor;
@@ -76,7 +76,7 @@
             Optional<SplitScreenController> splitScreenOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             FullscreenTaskListener fullscreenTaskListener,
-            Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformTaskListener<?>> freeformTaskListenerOptional,
             Optional<RecentTasksController> recentTasks,
@@ -93,7 +93,7 @@
         mSplitScreenOptional = splitScreenOptional;
         mFullscreenTaskListener = fullscreenTaskListener;
         mPipTouchHandlerOptional = pipTouchHandlerOptional;
-        mFullscreenUnfoldController = fullscreenUnfoldTransitionController;
+        mUnfoldController = unfoldAnimationController;
         mUnfoldTransitionHandler = unfoldTransitionHandler;
         mFreeformTaskListenerOptional = freeformTaskListenerOptional;
         mRecentTasks = recentTasks;
@@ -146,7 +146,7 @@
                 mShellTaskOrganizer.addListenerForType(
                         f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
 
-        mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init);
+        mUnfoldController.ifPresent(UnfoldAnimationController::init);
         mRecentTasks.ifPresent(RecentTasksController::init);
 
         // Initialize kids mode task organizer
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 31f0ef0..e9d24fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -55,6 +55,7 @@
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -179,33 +180,41 @@
     private final Optional<RecentTasksController> mRecentTasks;
 
     @Nullable
+    private final UnfoldAnimationController mUnfoldAnimationController;
+
+    @Nullable
     private RunningTaskInfo mLastFocusedTaskInfo;
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
         this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */,
+                Optional.empty() /* unfoldAnimationController */,
                 Optional.empty() /* recentTasksController */);
     }
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
             CompatUIController compatUI) {
         this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
+                Optional.empty() /* unfoldAnimationController */,
                 Optional.empty() /* recentTasksController */);
     }
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
             CompatUIController compatUI,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks) {
         this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
-                recentTasks);
+                unfoldAnimationController, recentTasks);
     }
 
     @VisibleForTesting
     protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController,
             ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks) {
         super(taskOrganizerController, mainExecutor);
         mCompatUI = compatUI;
         mRecentTasks = recentTasks;
+        mUnfoldAnimationController = unfoldAnimationController.orElse(null);
         if (compatUI != null) {
             compatUI.setCompatUICallback(this);
         }
@@ -437,6 +446,9 @@
         if (listener != null) {
             listener.onTaskAppeared(info.getTaskInfo(), info.getLeash());
         }
+        if (mUnfoldAnimationController != null) {
+            mUnfoldAnimationController.onTaskAppeared(info.getTaskInfo(), info.getLeash());
+        }
         notifyLocusVisibilityIfNeeded(info.getTaskInfo());
         notifyCompatUI(info.getTaskInfo(), listener);
     }
@@ -458,6 +470,11 @@
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
         synchronized (mLock) {
             ProtoLog.v(WM_SHELL_TASK_ORG, "Task info changed taskId=%d", taskInfo.taskId);
+
+            if (mUnfoldAnimationController != null) {
+                mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+            }
+
             final TaskAppearedInfo data = mTasks.get(taskInfo.taskId);
             final TaskListener oldListener = getTaskListener(data.getTaskInfo());
             final TaskListener newListener = getTaskListener(taskInfo);
@@ -507,6 +524,10 @@
     public void onTaskVanished(RunningTaskInfo taskInfo) {
         synchronized (mLock) {
             ProtoLog.v(WM_SHELL_TASK_ORG, "Task vanished taskId=%d", taskInfo.taskId);
+            if (mUnfoldAnimationController != null) {
+                mUnfoldAnimationController.onTaskVanished(taskInfo);
+            }
+
             final int taskId = taskInfo.taskId;
             final TaskListener listener = getTaskListener(mTasks.get(taskId).getTaskInfo());
             mTasks.remove(taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 7760df1..0cb56d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -210,19 +210,19 @@
         @Override
         public void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
             executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
-                    (controller) -> mController.setBackToLauncherCallback(callback));
+                    (controller) -> controller.setBackToLauncherCallback(callback));
         }
 
         @Override
         public void clearBackToLauncherCallback() {
             executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback",
-                    (controller) -> mController.clearBackToLauncherCallback());
+                    (controller) -> controller.clearBackToLauncherCallback());
         }
 
         @Override
         public void onBackToLauncherAnimationFinished() {
             executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished",
-                    (controller) -> mController.onBackToLauncherAnimationFinished());
+                    (controller) -> controller.onBackToLauncherAnimationFinished());
         }
 
         void invalidate() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 492bff9..66be2a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -137,6 +137,9 @@
 
     private static final float SCRIM_ALPHA = 0.6f;
 
+    /** Minimum alpha value for scrim when alpha is being changed via drag */
+    private static final float MIN_SCRIM_ALPHA_FOR_DRAG = 0.2f;
+
     /**
      * How long to wait to animate the stack temporarily invisible after a drag/flyout hide
      * animation ends, if we are in fact temporarily invisible.
@@ -214,6 +217,7 @@
     private ExpandedViewAnimationController mExpandedViewAnimationController;
 
     private View mScrim;
+    private boolean mScrimAnimating;
     private View mManageMenuScrim;
     private FrameLayout mExpandedViewContainer;
 
@@ -750,9 +754,14 @@
             if (mShowingManage) {
                 showManageMenu(false /* show */);
             }
-            // Only allow up
-            float collapsed = Math.min(dy, 0);
-            mExpandedViewAnimationController.updateDrag((int) -collapsed);
+            // Only allow up, normalize for up direction
+            float collapsed = -Math.min(dy, 0);
+            mExpandedViewAnimationController.updateDrag((int) collapsed);
+
+            // Update scrim
+            if (!mScrimAnimating) {
+                mScrim.setAlpha(getScrimAlphaForDrag(collapsed));
+            }
         }
 
         @Override
@@ -768,8 +777,26 @@
                 mBubbleData.setExpanded(false);
             } else {
                 mExpandedViewAnimationController.animateBackToExpanded();
+
+                // Update scrim
+                if (!mScrimAnimating) {
+                    showScrim(true);
+                }
             }
         }
+
+        private float getScrimAlphaForDrag(float dragAmount) {
+            // dragAmount should be negative as we allow scroll up only
+            if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                float alphaRange = SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG;
+
+                int dragMax = mExpandedBubble.getExpandedView().getContentHeight();
+                float dragFraction = dragAmount / dragMax;
+
+                return Math.max(SCRIM_ALPHA - alphaRange * dragFraction, MIN_SCRIM_ALPHA_FOR_DRAG);
+            }
+            return SCRIM_ALPHA;
+        }
     };
 
     /** Click listener set on the flyout, which expands the stack when the flyout is tapped. */
@@ -2125,15 +2152,28 @@
     }
 
     private void showScrim(boolean show) {
+        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mScrimAnimating = true;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mScrimAnimating = false;
+            }
+        };
         if (show) {
             mScrim.animate()
                     .setInterpolator(ALPHA_IN)
                     .alpha(SCRIM_ALPHA)
+                    .setListener(listener)
                     .start();
         } else {
             mScrim.animate()
                     .alpha(0f)
                     .setInterpolator(ALPHA_OUT)
+                    .setListener(listener)
                     .start();
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index a60c9ac..1724180 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -178,6 +178,11 @@
         return outBounds;
     }
 
+    /** Gets root bounds of the whole split layout */
+    public Rect getRootBounds() {
+        return new Rect(mRootBounds);
+    }
+
     /** Gets bounds of divider window with screen based coordinate. */
     public Rect getDividerBounds() {
         return new Rect(mDividerBounds);
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 12bff8d..2ea111b 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
@@ -64,7 +64,6 @@
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformTaskListener;
 import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
 import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
 import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
@@ -88,6 +87,7 @@
 import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 
 import java.util.Optional;
@@ -176,9 +176,11 @@
     static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor,
             Context context,
             CompatUIController compatUI,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasksOptional
     ) {
-        return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional);
+        return new ShellTaskOrganizer(mainExecutor, context, compatUI, unfoldAnimationController,
+                recentTasksOptional);
     }
 
     @WMSingleton
@@ -190,10 +192,12 @@
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasksOptional
     ) {
         return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue,
-                displayController, displayInsetsController, recentTasksOptional);
+                displayController, displayInsetsController, unfoldAnimationController,
+                recentTasksOptional);
     }
 
     @WMSingleton
@@ -290,13 +294,11 @@
     static FullscreenTaskListener provideFullscreenTaskListener(
             @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener,
             SyncTransactionQueue syncQueue,
-            Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController,
             Optional<RecentTasksController> recentTasksOptional) {
         if (fullscreenTaskListener.isPresent()) {
             return fullscreenTaskListener.get();
         } else {
-            return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController,
-                    recentTasksOptional);
+            return new FullscreenTaskListener(syncQueue, recentTasksOptional);
         }
     }
 
@@ -310,12 +312,13 @@
     // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
     @BindsOptionalOf
     @DynamicOverride
-    abstract FullscreenUnfoldController optionalFullscreenUnfoldController();
+    abstract UnfoldAnimationController optionalUnfoldController();
 
     @WMSingleton
     @Provides
-    static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController(
-            @DynamicOverride Lazy<Optional<FullscreenUnfoldController>> fullscreenUnfoldController,
+    static Optional<UnfoldAnimationController> provideUnfoldController(
+            @DynamicOverride Lazy<Optional<UnfoldAnimationController>>
+                    fullscreenUnfoldController,
             Optional<ShellUnfoldProgressProvider> progressProvider) {
         if (progressProvider.isPresent()
                 && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) {
@@ -640,7 +643,7 @@
             Optional<SplitScreenController> splitScreenOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             FullscreenTaskListener fullscreenTaskListener,
-            Optional<FullscreenUnfoldController> appUnfoldTransitionController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformTaskListener<?>> freeformTaskListener,
             Optional<RecentTasksController> recentTasksOptional,
@@ -657,7 +660,7 @@
                 splitScreenOptional,
                 pipTouchHandlerOptional,
                 fullscreenTaskListener,
-                appUnfoldTransitionController,
+                unfoldAnimationController,
                 unfoldTransitionHandler,
                 freeformTaskListener,
                 recentTasksOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index f59e045..1e36989 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -45,7 +45,6 @@
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.freeform.FreeformTaskListener;
-import com.android.wm.shell.fullscreen.FullscreenUnfoldController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.pip.PipAnimationController;
@@ -68,19 +67,24 @@
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
-import com.android.wm.shell.splitscreen.StageTaskUnfoldController;
 import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
 import com.android.wm.shell.unfold.UnfoldBackgroundController;
 import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
+import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+import com.android.wm.shell.unfold.qualifier.UnfoldTransition;
+import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition;
 import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Optional;
 
-import javax.inject.Provider;
-
+import dagger.Binds;
 import dagger.Lazy;
 import dagger.Module;
 import dagger.Provides;
@@ -94,7 +98,7 @@
  * dependencies should go into {@link WMShellBaseModule}.
  */
 @Module(includes = WMShellBaseModule.class)
-public class WMShellModule {
+public abstract class WMShellModule {
 
     //
     // Bubbles
@@ -196,12 +200,11 @@
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, IconProvider iconProvider,
-            Optional<RecentTasksController> recentTasks,
-            Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
+            Optional<RecentTasksController> recentTasks) {
         return new SplitScreenController(shellTaskOrganizer, syncQueue, context,
                 rootTaskDisplayAreaOrganizer, mainExecutor, displayController, displayImeController,
                 displayInsetsController, transitions, transactionPool, iconProvider,
-                recentTasks, stageTaskUnfoldControllerProvider);
+                recentTasks);
     }
 
     //
@@ -356,62 +359,77 @@
     //
     // Unfold transition
     //
-
     @WMSingleton
     @Provides
     @DynamicOverride
-    static FullscreenUnfoldController provideFullscreenUnfoldController(
+    static UnfoldAnimationController provideUnfoldAnimationController(
             Optional<ShellUnfoldProgressProvider> progressProvider,
-            Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
-            FullscreenUnfoldTaskAnimator fullscreenUnfoldTaskAnimator,
-            UnfoldBackgroundController unfoldBackgroundController,
+            TransactionPool transactionPool,
+            @UnfoldTransition SplitTaskUnfoldAnimator splitAnimator,
+            FullscreenUnfoldTaskAnimator fullscreenAnimator,
+            Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
-        return new FullscreenUnfoldController(mainExecutor,
-                unfoldBackgroundController, progressProvider.get(),
-                unfoldTransitionHandler.get(), fullscreenUnfoldTaskAnimator);
+        final List<UnfoldTaskAnimator> animators = new ArrayList<>();
+        animators.add(splitAnimator);
+        animators.add(fullscreenAnimator);
+
+        return new UnfoldAnimationController(
+                        transactionPool,
+                        progressProvider.get(),
+                        animators,
+                        unfoldTransitionHandler,
+                        mainExecutor
+                );
     }
 
+
     @Provides
     static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator(
             Context context,
+            UnfoldBackgroundController unfoldBackgroundController,
             DisplayInsetsController displayInsetsController
     ) {
-        return new FullscreenUnfoldTaskAnimator(context, displayInsetsController);
+        return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController,
+                displayInsetsController);
     }
 
+    @Provides
+    static SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimatorBase(
+            Context context,
+            UnfoldBackgroundController backgroundController,
+            @ShellMainThread ShellExecutor executor,
+            Lazy<Optional<SplitScreenController>> splitScreenOptional,
+            DisplayInsetsController displayInsetsController
+    ) {
+        return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional,
+                backgroundController, displayInsetsController);
+    }
+
+    @WMSingleton
+    @UnfoldShellTransition
+    @Binds
+    abstract SplitTaskUnfoldAnimator provideShellSplitTaskUnfoldAnimator(
+            SplitTaskUnfoldAnimator splitTaskUnfoldAnimator);
+
+    @WMSingleton
+    @UnfoldTransition
+    @Binds
+    abstract SplitTaskUnfoldAnimator provideSplitTaskUnfoldAnimator(
+            SplitTaskUnfoldAnimator splitTaskUnfoldAnimator);
+
     @WMSingleton
     @Provides
     @DynamicOverride
     static UnfoldTransitionHandler provideUnfoldTransitionHandler(
             Optional<ShellUnfoldProgressProvider> progressProvider,
             FullscreenUnfoldTaskAnimator animator,
-            UnfoldBackgroundController backgroundController,
+            @UnfoldShellTransition SplitTaskUnfoldAnimator unfoldAnimator,
             TransactionPool transactionPool,
             Transitions transitions,
             @ShellMainThread ShellExecutor executor) {
         return new UnfoldTransitionHandler(progressProvider.get(), animator,
-                transactionPool, backgroundController, executor, transitions);
-    }
-
-    @Provides
-    static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController(
-            Optional<ShellUnfoldProgressProvider> progressProvider,
-            Context context,
-            TransactionPool transactionPool,
-            Lazy<UnfoldBackgroundController> unfoldBackgroundController,
-            DisplayInsetsController displayInsetsController,
-            @ShellMainThread ShellExecutor mainExecutor
-    ) {
-        return progressProvider.map(shellUnfoldTransitionProgressProvider ->
-                new StageTaskUnfoldController(
-                        context,
-                        transactionPool,
-                        shellUnfoldTransitionProgressProvider,
-                        displayInsetsController,
-                        unfoldBackgroundController.get(),
-                        mainExecutor
-                ));
+                unfoldAnimator, transactionPool, executor, transitions);
     }
 
     @WMSingleton
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 1fc1215..79e363b 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
@@ -16,17 +16,13 @@
 
 package com.android.wm.shell.fullscreen;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-
 import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
 import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString;
 
 import android.app.ActivityManager.RunningTaskInfo;
-import android.app.TaskInfo;
 import android.graphics.Point;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseBooleanArray;
 import android.view.SurfaceControl;
 
 import androidx.annotation.NonNull;
@@ -48,22 +44,17 @@
     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();
 
-    public FullscreenTaskListener(SyncTransactionQueue syncQueue,
-            Optional<FullscreenUnfoldController> unfoldController) {
-        this(syncQueue, unfoldController, Optional.empty());
+    public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
+        this(syncQueue, Optional.empty());
     }
 
     public FullscreenTaskListener(SyncTransactionQueue syncQueue,
-            Optional<FullscreenUnfoldController> unfoldController,
             Optional<RecentTasksController> recentTasks) {
         mSyncQueue = syncQueue;
-        mFullscreenUnfoldController = unfoldController.orElse(null);
         mRecentTasksOptional = recentTasks;
     }
 
@@ -76,7 +67,6 @@
                 taskInfo.taskId);
         final Point positionInParent = taskInfo.positionInParent;
         mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent));
-        mAnimatableTasksListener.onTaskAppeared(taskInfo);
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
         mSyncQueue.runInSync(t -> {
@@ -94,8 +84,6 @@
 
     @Override
     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
-        mAnimatableTasksListener.onTaskInfoChanged(taskInfo);
-
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
 
         updateRecentsForVisibleFullscreenTask(taskInfo);
@@ -117,7 +105,6 @@
             return;
         }
 
-        mAnimatableTasksListener.onTaskVanished(taskInfo);
         mDataByTaskId.remove(taskInfo.taskId);
 
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
@@ -175,65 +162,4 @@
             this.positionInParent = positionInParent;
         }
     }
-
-    class AnimatableTasksListener {
-        private final SparseBooleanArray mTaskIds = new SparseBooleanArray();
-
-        public void onTaskAppeared(RunningTaskInfo taskInfo) {
-            final boolean isApplicable = isAnimatable(taskInfo);
-            if (isApplicable) {
-                mTaskIds.put(taskInfo.taskId, true);
-
-                if (mFullscreenUnfoldController != null) {
-                    SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
-                    mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
-                }
-            }
-        }
-
-        public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
-            final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
-            final boolean isApplicable = isAnimatable(taskInfo);
-
-            if (isCurrentlyApplicable) {
-                if (isApplicable) {
-                    // Still applicable, send update
-                    if (mFullscreenUnfoldController != null) {
-                        mFullscreenUnfoldController.onTaskInfoChanged(taskInfo);
-                    }
-                } else {
-                    // Became inapplicable
-                    if (mFullscreenUnfoldController != null) {
-                        mFullscreenUnfoldController.onTaskVanished(taskInfo);
-                    }
-                    mTaskIds.put(taskInfo.taskId, false);
-                }
-            } else {
-                if (isApplicable) {
-                    // Became applicable
-                    mTaskIds.put(taskInfo.taskId, true);
-
-                    if (mFullscreenUnfoldController != null) {
-                        SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface;
-                        mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash);
-                    }
-                }
-            }
-        }
-
-        public void onTaskVanished(RunningTaskInfo taskInfo) {
-            final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId);
-            if (isCurrentlyApplicable && mFullscreenUnfoldController != null) {
-                mFullscreenUnfoldController.onTaskVanished(taskInfo);
-            }
-            mTaskIds.put(taskInfo.taskId, false);
-        }
-
-        private boolean isAnimatable(TaskInfo taskInfo) {
-            // Filter all visible tasks that are not launcher tasks
-            // We do not animate launcher as it handles the animation by itself
-            return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration()
-                    .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME;
-        }
-    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
deleted file mode 100644
index 99f15f6..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java
+++ /dev/null
@@ -1,135 +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.wm.shell.fullscreen;
-
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.view.SurfaceControl;
-
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
-import com.android.wm.shell.unfold.UnfoldBackgroundController;
-import com.android.wm.shell.unfold.UnfoldTransitionHandler;
-import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
-
-import java.util.concurrent.Executor;
-
-/**
- * Controls full screen app unfold transition: animating cropping window and scaling when
- * folding or unfolding a foldable device.
- *
- * - When Shell transitions are disabled (legacy mode) this controller animates task surfaces
- *   when doing both fold and unfold.
- *
- * - When Shell transitions are enabled this controller animates the surfaces only when
- *   folding a foldable device. It's not done as a shell transition because we are not committed
- *   to the display size WM changes yet.
- *   In this case unfolding is handled by
- *   {@link com.android.wm.shell.unfold.UnfoldTransitionHandler}.
- */
-public final class FullscreenUnfoldController implements UnfoldListener {
-
-    private final Executor mExecutor;
-    private final ShellUnfoldProgressProvider mProgressProvider;
-    private final UnfoldBackgroundController mBackgroundController;
-    private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-    private final FullscreenUnfoldTaskAnimator mAnimator;
-    private final UnfoldTransitionHandler mUnfoldTransitionHandler;
-
-    private boolean mShouldHandleAnimation = false;
-
-    public FullscreenUnfoldController(
-            @NonNull Executor executor,
-            @NonNull UnfoldBackgroundController backgroundController,
-            @NonNull ShellUnfoldProgressProvider progressProvider,
-            @NonNull UnfoldTransitionHandler unfoldTransitionHandler,
-            @NonNull FullscreenUnfoldTaskAnimator animator
-    ) {
-        mExecutor = executor;
-        mProgressProvider = progressProvider;
-        mBackgroundController = backgroundController;
-        mUnfoldTransitionHandler = unfoldTransitionHandler;
-        mAnimator = animator;
-    }
-
-    /**
-     * Initializes the controller
-     */
-    public void init() {
-        mAnimator.init();
-        mProgressProvider.addListener(mExecutor, this);
-    }
-
-    @Override
-    public void onStateChangeStarted() {
-        mShouldHandleAnimation = !mUnfoldTransitionHandler.willHandleTransition();
-    }
-
-    @Override
-    public void onStateChangeProgress(float progress) {
-        if (!mAnimator.hasActiveTasks() || !mShouldHandleAnimation) return;
-
-        mBackgroundController.ensureBackground(mTransaction);
-        mAnimator.applyAnimationProgress(progress, mTransaction);
-        mTransaction.apply();
-    }
-
-    @Override
-    public void onStateChangeFinished() {
-        if (!mShouldHandleAnimation) {
-            return;
-        }
-
-        mShouldHandleAnimation = false;
-        mAnimator.resetAllSurfaces(mTransaction);
-        mBackgroundController.removeBackground(mTransaction);
-        mTransaction.apply();
-    }
-
-    /**
-     * Called when a new matching task appeared
-     */
-    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
-        mAnimator.addTask(taskInfo, leash);
-    }
-
-    /**
-     * Called when matching task changed
-     */
-    public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
-        mAnimator.onTaskInfoChanged(taskInfo);
-    }
-
-    /**
-     * Called when matching task vanished
-     */
-    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
-        // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
-        if (taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED) {
-            mAnimator.resetSurface(taskInfo, mTransaction);
-        }
-        mAnimator.removeTask(taskInfo);
-
-        if (!mAnimator.hasActiveTasks()) {
-            mBackgroundController.removeBackground(mTransaction);
-        }
-
-        mTransaction.apply();
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
index b4c87b6..2c8ba09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
@@ -51,6 +51,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
 
 import java.io.PrintWriter;
 import java.util.List;
@@ -146,9 +147,11 @@
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
             KidsModeSettingsObserver kidsModeSettingsObserver) {
-        super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null, recentTasks);
+        super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null,
+                unfoldAnimationController, recentTasks);
         mContext = context;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
@@ -164,8 +167,9 @@
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
+            Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks) {
-        super(mainExecutor, context, /* compatUI= */ null, recentTasks);
+        super(mainExecutor, context, /* compatUI= */ null, unfoldAnimationController, recentTasks);
         mContext = context;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index ae5e075..2bfa5db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.splitscreen;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -38,10 +37,9 @@
 
     MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider,
-            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
-        super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider,
-                stageTaskUnfoldController);
+            SurfaceSession surfaceSession, IconProvider iconProvider) {
+        super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                iconProvider);
     }
 
     boolean isActive() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
index d55619f..f92a0d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.splitscreen;
 
-import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.content.Context;
 import android.view.SurfaceSession;
@@ -38,10 +37,9 @@
 
     SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider,
-            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
-        super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider,
-                stageTaskUnfoldController);
+            SurfaceSession surfaceSession, IconProvider iconProvider) {
+        super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession,
+                iconProvider);
     }
 
     boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 448773a..29b6311 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.graphics.Rect;
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
@@ -58,6 +59,7 @@
     interface SplitScreenListener {
         default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {}
         default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {}
+        default void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {}
         default void onSplitVisibilityChanged(boolean visible) {}
     }
 
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 af0e465..ee49366 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
@@ -87,8 +87,6 @@
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
-import javax.inject.Provider;
-
 /**
  * Class manages split-screen multitasking mode and implements the main interface
  * {@link SplitScreen}.
@@ -139,7 +137,6 @@
     private final SplitscreenEventLogger mLogger;
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
-    private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
 
     private StageCoordinator mStageCoordinator;
     // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
@@ -153,8 +150,7 @@
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
             Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
-            Optional<RecentTasksController> recentTasks,
-            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+            Optional<RecentTasksController> recentTasks) {
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
         mContext = context;
@@ -165,7 +161,6 @@
         mDisplayInsetsController = displayInsetsController;
         mTransitions = transitions;
         mTransactionPool = transactionPool;
-        mUnfoldControllerProvider = unfoldControllerProvider;
         mLogger = new SplitscreenEventLogger();
         mIconProvider = iconProvider;
         mRecentTasksOptional = recentTasks;
@@ -191,7 +186,7 @@
             mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                     mTaskOrganizer, mDisplayController, mDisplayImeController,
                     mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
-                    mIconProvider, mMainExecutor, mRecentTasksOptional, mUnfoldControllerProvider);
+                    mIconProvider, mMainExecutor, mRecentTasksOptional);
         }
     }
 
@@ -227,6 +222,14 @@
                 new WindowContainerTransaction());
     }
 
+    /**
+     * Update surfaces of the split screen layout based on the current state
+     * @param transaction to write the updates to
+     */
+    public void updateSplitScreenSurfaces(SurfaceControl.Transaction transaction) {
+        mStageCoordinator.updateSurfaces(transaction);
+    }
+
     private boolean moveToStage(int taskId, @StageType int stageType,
             @SplitPosition int stagePosition, WindowContainerTransaction wct) {
         final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId);
@@ -541,6 +544,17 @@
             }
 
             @Override
+            public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {
+                for (int i = 0; i < mExecutors.size(); i++) {
+                    final int index = i;
+                    mExecutors.valueAt(index).execute(() -> {
+                        mExecutors.keyAt(index).onSplitBoundsChanged(rootBounds, mainBounds,
+                                sideBounds);
+                    });
+                }
+            }
+
+            @Override
             public void onSplitVisibilityChanged(boolean visible) {
                 for (int i = 0; i < mExecutors.size(); i++) {
                     final int index = i;
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 0517995..774d6ae 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
@@ -118,8 +118,6 @@
 import java.util.List;
 import java.util.Optional;
 
-import javax.inject.Provider;
-
 /**
  * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
  * {@link SideStage} stages.
@@ -146,10 +144,8 @@
 
     private final MainStage mMainStage;
     private final StageListenerImpl mMainStageListener = new StageListenerImpl();
-    private final StageTaskUnfoldController mMainUnfoldController;
     private final SideStage mSideStage;
     private final StageListenerImpl mSideStageListener = new StageListenerImpl();
-    private final StageTaskUnfoldController mSideUnfoldController;
     private final DisplayLayout mDisplayLayout;
     @SplitPosition
     private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -210,8 +206,7 @@
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, SplitscreenEventLogger logger,
             IconProvider iconProvider, ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks,
-            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+            Optional<RecentTasksController> recentTasks) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -219,8 +214,7 @@
         mLogger = logger;
         mMainExecutor = mainExecutor;
         mRecentTasks = recentTasks;
-        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
-        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
+
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
 
         mMainStage = new MainStage(
@@ -230,8 +224,7 @@
                 mMainStageListener,
                 mSyncQueue,
                 mSurfaceSession,
-                iconProvider,
-                mMainUnfoldController);
+                iconProvider);
         mSideStage = new SideStage(
                 mContext,
                 mTaskOrganizer,
@@ -239,8 +232,7 @@
                 mSideStageListener,
                 mSyncQueue,
                 mSurfaceSession,
-                iconProvider,
-                mSideUnfoldController);
+                iconProvider);
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
@@ -263,8 +255,7 @@
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
             Transitions transitions, TransactionPool transactionPool,
             SplitscreenEventLogger logger, ShellExecutor mainExecutor,
-            Optional<RecentTasksController> recentTasks,
-            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+            Optional<RecentTasksController> recentTasks) {
         mContext = context;
         mDisplayId = displayId;
         mSyncQueue = syncQueue;
@@ -278,8 +269,6 @@
         mSplitLayout = splitLayout;
         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
                 this::onTransitionAnimationComplete, this);
-        mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
-        mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
         mLogger = logger;
         mMainExecutor = mainExecutor;
         mRecentTasks = recentTasks;
@@ -636,7 +625,7 @@
                 onLayoutSizeChanged(mSplitLayout);
             } else {
                 updateWindowBounds(mSplitLayout, wct);
-                updateUnfoldBounds();
+                sendOnBoundsChanged();
             }
         }
     }
@@ -866,6 +855,10 @@
         listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
         listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
         listener.onSplitVisibilityChanged(isSplitScreenVisible());
+        if (mSplitLayout != null) {
+            listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(),
+                    getSideStageBounds());
+        }
         mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
         mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
     }
@@ -878,6 +871,14 @@
         }
     }
 
+    private void sendOnBoundsChanged() {
+        if (mSplitLayout == null) return;
+        for (int i = mListeners.size() - 1; i >= 0; --i) {
+            mListeners.get(i).onSplitBoundsChanged(mSplitLayout.getRootBounds(),
+                    getMainStageBounds(), getSideStageBounds());
+        }
+    }
+
     private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
             boolean present, boolean visible) {
         int stage;
@@ -941,12 +942,7 @@
             final SplitScreen.SplitScreenListener l = mListeners.get(i);
             l.onSplitVisibilityChanged(mDividerVisible);
         }
-
-        if (mMainUnfoldController != null && mSideUnfoldController != null) {
-            mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible);
-            mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible);
-            updateUnfoldBounds();
-        }
+        sendOnBoundsChanged();
     }
 
     @Override
@@ -967,11 +963,6 @@
             mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
         }
 
-        if (mMainUnfoldController != null && mSideUnfoldController != null) {
-            mMainUnfoldController.init();
-            mSideUnfoldController.init();
-        }
-
         onRootTaskAppeared();
     }
 
@@ -985,13 +976,8 @@
         mRootTaskInfo = taskInfo;
         if (mSplitLayout != null
                 && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)
-                && mMainStage.isActive()) {
-            // TODO(b/204925795): With Shell transition, We are handling split bounds rotation at
-            //  onRotateDisplay. But still need to handle unfold case.
-            if (ENABLE_SHELL_TRANSITIONS) {
-                updateUnfoldBounds();
-                return;
-            }
+                && mMainStage.isActive()
+                && !ENABLE_SHELL_TRANSITIONS) {
             // Clear the divider remote animating flag as the divider will be re-rendered to apply
             // the new rotation config.
             mIsDividerRemoteAnimating = false;
@@ -1241,7 +1227,7 @@
     public void onLayoutSizeChanged(SplitLayout layout) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         updateWindowBounds(layout, wct);
-        updateUnfoldBounds();
+        sendOnBoundsChanged();
         mSyncQueue.queue(wct);
         mSyncQueue.runInSync(t -> {
             setResizingSplits(false /* resizing */);
@@ -1252,15 +1238,6 @@
         mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
     }
 
-    private void updateUnfoldBounds() {
-        if (mMainUnfoldController != null && mSideUnfoldController != null) {
-            mMainUnfoldController.onLayoutChanged(getMainStageBounds(), getMainStagePosition(),
-                    isLandscape());
-            mSideUnfoldController.onLayoutChanged(getSideStageBounds(), getSideStagePosition(),
-                    isLandscape());
-        }
-    }
-
     private boolean isLandscape() {
         return mSplitLayout.isLandscape();
     }
@@ -1340,6 +1317,11 @@
         mDisplayLayout.set(mDisplayController.getDisplayLayout(displayId));
     }
 
+    void updateSurfaces(SurfaceControl.Transaction transaction) {
+        updateSurfaceBounds(mSplitLayout, transaction, /* applyResizingOffset */ false);
+        mSplitLayout.update(transaction);
+    }
+
     private void onDisplayChange(int displayId, int fromRotation, int toRotation,
             @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
         if (!mMainStage.isActive()) return;
@@ -1352,7 +1334,7 @@
             mSplitLayout.updateConfiguration(newDisplayAreaInfo.configuration);
         }
         updateWindowBounds(mSplitLayout, wct);
-        updateUnfoldBounds();
+        sendOnBoundsChanged();
     }
 
     private void onFoldedStateChanged(boolean folded) {
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 f9dd7c96..23eec96a 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
@@ -97,18 +97,14 @@
     // TODO(b/204308910): Extracts SplitDecorManager related code to common package.
     private SplitDecorManager mSplitDecorManager;
 
-    private final StageTaskUnfoldController mStageTaskUnfoldController;
-
     StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider,
-            @Nullable StageTaskUnfoldController stageTaskUnfoldController) {
+            SurfaceSession surfaceSession, IconProvider iconProvider) {
         mContext = context;
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
         mSurfaceSession = surfaceSession;
         mIconProvider = iconProvider;
-        mStageTaskUnfoldController = stageTaskUnfoldController;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
     }
 
@@ -199,10 +195,6 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
-
-        if (mStageTaskUnfoldController != null) {
-            mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash);
-        }
     }
 
     @Override
@@ -270,10 +262,6 @@
             throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
                     + "\n mRootTaskInfo: " + mRootTaskInfo);
         }
-
-        if (mStageTaskUnfoldController != null) {
-            mStageTaskUnfoldController.onTaskVanished(taskInfo);
-        }
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
deleted file mode 100644
index 59eecb5d..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java
+++ /dev/null
@@ -1,281 +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.wm.shell.splitscreen;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-
-import android.animation.RectEvaluator;
-import android.animation.TypeEvaluator;
-import android.annotation.NonNull;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.util.SparseArray;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.SurfaceControl;
-
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.wm.shell.common.DisplayInsetsController;
-import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
-import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
-import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
-import com.android.wm.shell.unfold.UnfoldBackgroundController;
-
-import java.util.concurrent.Executor;
-
-/**
- * Controls transformations of the split screen task surfaces in response
- * to the unfolding/folding action on foldable devices
- */
-public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener {
-
-    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
-    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
-
-    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
-    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
-    private final DisplayInsetsController mDisplayInsetsController;
-    private final UnfoldBackgroundController mBackgroundController;
-    private final Executor mExecutor;
-    private final int mExpandedTaskBarHeight;
-    private final float mWindowCornerRadiusPx;
-    private final Rect mStageBounds = new Rect();
-    private final TransactionPool mTransactionPool;
-
-    private InsetsSource mTaskbarInsetsSource;
-    private boolean mBothStagesVisible;
-
-    public StageTaskUnfoldController(@NonNull Context context,
-            @NonNull TransactionPool transactionPool,
-            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
-            @NonNull DisplayInsetsController displayInsetsController,
-            @NonNull UnfoldBackgroundController backgroundController,
-            @NonNull Executor executor) {
-        mUnfoldProgressProvider = unfoldProgressProvider;
-        mTransactionPool = transactionPool;
-        mExecutor = executor;
-        mBackgroundController = backgroundController;
-        mDisplayInsetsController = displayInsetsController;
-        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
-        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.taskbar_frame_height);
-    }
-
-    /**
-     * Initializes the controller, starts listening for the external events
-     */
-    public void init() {
-        mUnfoldProgressProvider.addListener(mExecutor, this);
-        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
-    }
-
-    @Override
-    public void insetsChanged(InsetsState insetsState) {
-        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
-        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
-            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-            context.update();
-        }
-    }
-
-    /**
-     * Called when split screen task appeared
-     * @param taskInfo info for the appeared task
-     * @param leash surface leash for the appeared task
-     */
-    public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
-        // Only handle child task surface here.
-        if (!taskInfo.hasParentTask()) return;
-
-        AnimationContext context = new AnimationContext(leash);
-        mAnimationContextByTaskId.put(taskInfo.taskId, context);
-    }
-
-    /**
-     * Called when a split screen task vanished
-     * @param taskInfo info for the vanished task
-     */
-    public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
-        if (!taskInfo.hasParentTask()) return;
-
-        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
-        if (context != null) {
-            final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-            resetSurface(transaction, context);
-            transaction.apply();
-            mTransactionPool.release(transaction);
-        }
-        mAnimationContextByTaskId.remove(taskInfo.taskId);
-    }
-
-    @Override
-    public void onStateChangeProgress(float progress) {
-        if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return;
-
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-        mBackgroundController.ensureBackground(transaction);
-
-        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
-            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-
-            context.mCurrentCropRect.set(RECT_EVALUATOR
-                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
-
-            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
-                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
-        }
-
-        transaction.apply();
-
-        mTransactionPool.release(transaction);
-    }
-
-    @Override
-    public void onStateChangeFinished() {
-        resetTransformations();
-    }
-
-    /**
-     * Called when split screen visibility changes
-     * @param bothStagesVisible true if both stages of the split screen are visible
-     */
-    public void onSplitVisibilityChanged(boolean bothStagesVisible) {
-        mBothStagesVisible = bothStagesVisible;
-        if (!bothStagesVisible) {
-            resetTransformations();
-        }
-    }
-
-    /**
-     * Called when split screen stage bounds changed
-     * @param bounds new bounds for this stage
-     */
-    public void onLayoutChanged(Rect bounds, @SplitPosition int splitPosition,
-            boolean isLandscape) {
-        mStageBounds.set(bounds);
-
-        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
-            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-            context.update(splitPosition, isLandscape);
-        }
-    }
-
-    private void resetTransformations() {
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-
-        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
-            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
-            resetSurface(transaction, context);
-        }
-        mBackgroundController.removeBackground(transaction);
-        transaction.apply();
-
-        mTransactionPool.release(transaction);
-    }
-
-    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
-        transaction
-                .setWindowCrop(context.mLeash, null)
-                .setCornerRadius(context.mLeash, 0.0F);
-    }
-
-    private class AnimationContext {
-        final SurfaceControl mLeash;
-        final Rect mStartCropRect = new Rect();
-        final Rect mEndCropRect = new Rect();
-        final Rect mCurrentCropRect = new Rect();
-
-        private @SplitPosition int mSplitPosition = SPLIT_POSITION_UNDEFINED;
-        private boolean mIsLandscape = false;
-
-        private AnimationContext(SurfaceControl leash) {
-            this.mLeash = leash;
-            update();
-        }
-
-        private void update(@SplitPosition int splitPosition, boolean isLandscape) {
-            this.mSplitPosition = splitPosition;
-            this.mIsLandscape = isLandscape;
-            update();
-        }
-
-        private void update() {
-            mStartCropRect.set(mStageBounds);
-
-            boolean taskbarExpanded = isTaskbarExpanded();
-            if (taskbarExpanded) {
-                // Only insets the cropping window with taskbar when taskbar is expanded
-                mStartCropRect.inset(mTaskbarInsetsSource.calculateVisibleInsets(mStartCropRect));
-            }
-
-            // Offset to surface coordinates as layout bounds are in screen coordinates
-            mStartCropRect.offsetTo(0, 0);
-
-            mEndCropRect.set(mStartCropRect);
-
-            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
-            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
-
-            // Sides adjacent to split bar or task bar are not be animated.
-            Insets margins;
-            if (mIsLandscape) { // Left and right splits.
-                margins = getLandscapeMargins(margin, taskbarExpanded);
-            } else { // Top and bottom splits.
-                margins = getPortraitMargins(margin, taskbarExpanded);
-            }
-            mStartCropRect.inset(margins);
-        }
-
-        private Insets getLandscapeMargins(int margin, boolean taskbarExpanded) {
-            int left = margin;
-            int right = margin;
-            int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin.
-            if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
-                right = 0; // Divider margin.
-            } else {
-                left = 0; // Divider margin.
-            }
-            return Insets.of(left, /* top= */ margin, right, bottom);
-        }
-
-        private Insets getPortraitMargins(int margin, boolean taskbarExpanded) {
-            int bottom = margin;
-            int top = margin;
-            if (mSplitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
-                bottom = 0; // Divider margin.
-            } else { // Bottom split.
-                top = 0; // Divider margin.
-                if (taskbarExpanded) {
-                    bottom = 0; // Taskbar margin.
-                }
-            }
-            return Insets.of(/* left= */ margin, top, /* right= */ margin, bottom);
-        }
-
-        private boolean isTaskbarExpanded() {
-            return mTaskbarInsetsSource != null
-                    && mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight;
-        }
-    }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
new file mode 100644
index 0000000..530d474
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
+import android.util.SparseArray;
+import android.view.SurfaceControl;
+
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import dagger.Lazy;
+
+/**
+ * Manages fold/unfold animations of tasks on foldable devices.
+ * When folding or unfolding a foldable device we play animations that
+ * transform task cropping/scaling/rounded corners.
+ *
+ * This controller manages:
+ *  1) Folding/unfolding when Shell transitions disabled
+ *  2) Folding when Shell transitions enabled, unfolding is managed by
+ *    {@link com.android.wm.shell.unfold.UnfoldTransitionHandler}
+ */
+public class UnfoldAnimationController implements UnfoldListener {
+
+    private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
+    private final Executor mExecutor;
+    private final TransactionPool mTransactionPool;
+    private final List<UnfoldTaskAnimator> mAnimators;
+    private final Lazy<Optional<UnfoldTransitionHandler>> mUnfoldTransitionHandler;
+
+    private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
+    private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
+
+    public UnfoldAnimationController(@NonNull TransactionPool transactionPool,
+            @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
+            @NonNull List<UnfoldTaskAnimator> animators,
+            @NonNull Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
+            @NonNull Executor executor) {
+        mUnfoldProgressProvider = unfoldProgressProvider;
+        mUnfoldTransitionHandler = unfoldTransitionHandler;
+        mTransactionPool = transactionPool;
+        mExecutor = executor;
+        mAnimators = animators;
+    }
+
+    /**
+     * Initializes the controller, starts listening for the external events
+     */
+    public void init() {
+        mUnfoldProgressProvider.addListener(mExecutor, this);
+
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            animator.init();
+            animator.start();
+        }
+    }
+
+    /**
+     * Called when a task appeared
+     * @param taskInfo info for the appeared task
+     * @param leash surface leash for the appeared task
+     */
+    public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
+        mTaskSurfaces.put(taskInfo.taskId, leash);
+
+        // Find the first matching animator
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            if (animator.isApplicableTask(taskInfo)) {
+                mAnimatorsByTaskId.put(taskInfo.taskId, animator);
+                animator.onTaskAppeared(taskInfo, leash);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Called when task info changed
+     * @param taskInfo info for the changed task
+     */
+    public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
+        final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId);
+        final boolean isCurrentlyApplicable = animator != null;
+
+        if (isCurrentlyApplicable) {
+            final boolean isApplicable = animator.isApplicableTask(taskInfo);
+            if (isApplicable) {
+                // Still applicable, send update
+                animator.onTaskChanged(taskInfo);
+            } else {
+                // Became inapplicable
+                resetTask(animator, taskInfo);
+                animator.onTaskVanished(taskInfo);
+                mAnimatorsByTaskId.remove(taskInfo.taskId);
+            }
+        } else {
+            // Find the first matching animator
+            for (int i = 0; i < mAnimators.size(); i++) {
+                final UnfoldTaskAnimator currentAnimator = mAnimators.get(i);
+                if (currentAnimator.isApplicableTask(taskInfo)) {
+                    // Became applicable
+                    mAnimatorsByTaskId.put(taskInfo.taskId, currentAnimator);
+
+                    SurfaceControl leash = mTaskSurfaces.get(taskInfo.taskId);
+                    currentAnimator.onTaskAppeared(taskInfo, leash);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Called when a task vanished
+     * @param taskInfo info for the vanished task
+     */
+    public void onTaskVanished(RunningTaskInfo taskInfo) {
+        mTaskSurfaces.remove(taskInfo.taskId);
+
+        final UnfoldTaskAnimator animator = mAnimatorsByTaskId.get(taskInfo.taskId);
+        final boolean isCurrentlyApplicable = animator != null;
+
+        if (isCurrentlyApplicable) {
+            resetTask(animator, taskInfo);
+            animator.onTaskVanished(taskInfo);
+            mAnimatorsByTaskId.remove(taskInfo.taskId);
+        }
+    }
+
+    @Override
+    public void onStateChangeStarted() {
+        if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+            return;
+        }
+
+        SurfaceControl.Transaction transaction = null;
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            if (animator.hasActiveTasks()) {
+                if (transaction == null) transaction = mTransactionPool.acquire();
+                animator.prepareStartTransaction(transaction);
+            }
+        }
+
+        if (transaction != null) {
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+    }
+
+    @Override
+    public void onStateChangeProgress(float progress) {
+        if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+            return;
+        }
+
+        SurfaceControl.Transaction transaction = null;
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            if (animator.hasActiveTasks()) {
+                if (transaction == null) transaction = mTransactionPool.acquire();
+                animator.applyAnimationProgress(progress, transaction);
+            }
+        }
+
+        if (transaction != null) {
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
+    }
+
+    @Override
+    public void onStateChangeFinished() {
+        if (mUnfoldTransitionHandler.get().get().willHandleTransition()) {
+            return;
+        }
+
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            animator.resetAllSurfaces(transaction);
+            animator.prepareFinishTransaction(transaction);
+        }
+
+        transaction.apply();
+
+        mTransactionPool.release(transaction);
+    }
+
+    private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
+        animator.resetSurface(taskInfo, transaction);
+        transaction.apply();
+        mTransactionPool.release(transaction);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 8e45e7d..9bf32fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.unfold;
 
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
 import android.os.IBinder;
@@ -35,19 +33,22 @@
 import com.android.wm.shell.transition.Transitions.TransitionHandler;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
 import com.android.wm.shell.unfold.animation.FullscreenUnfoldTaskAnimator;
+import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
  * Transition handler that is responsible for animating app surfaces when unfolding of foldable
  * devices. It does not handle the folding animation, which is done in
- * {@link com.android.wm.shell.fullscreen.FullscreenUnfoldController}.
+ * {@link UnfoldAnimationController}.
  */
 public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListener {
 
     private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
     private final Transitions mTransitions;
-    private final UnfoldBackgroundController mUnfoldBackgroundController;
     private final Executor mExecutor;
     private final TransactionPool mTransactionPool;
 
@@ -56,22 +57,26 @@
     @Nullable
     private IBinder mTransition;
 
-    private final FullscreenUnfoldTaskAnimator mFullscreenAnimator;
+    private final List<UnfoldTaskAnimator> mAnimators = new ArrayList<>();
 
     public UnfoldTransitionHandler(ShellUnfoldProgressProvider unfoldProgressProvider,
-            FullscreenUnfoldTaskAnimator animator, TransactionPool transactionPool,
-            UnfoldBackgroundController unfoldBackgroundController,
+            FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator,
+            SplitTaskUnfoldAnimator splitUnfoldTaskAnimator,
+            TransactionPool transactionPool,
             Executor executor, Transitions transitions) {
         mUnfoldProgressProvider = unfoldProgressProvider;
-        mFullscreenAnimator = animator;
         mTransactionPool = transactionPool;
-        mUnfoldBackgroundController = unfoldBackgroundController;
         mExecutor = executor;
         mTransitions = transitions;
+
+        mAnimators.add(splitUnfoldTaskAnimator);
+        mAnimators.add(fullscreenUnfoldAnimator);
     }
 
     public void init() {
-        mFullscreenAnimator.init();
+        for (int i = 0; i < mAnimators.size(); i++) {
+            mAnimators.get(i).init();
+        }
         mTransitions.addHandler(this);
         mUnfoldProgressProvider.addListener(mExecutor, this);
     }
@@ -83,44 +88,67 @@
             @NonNull TransitionFinishCallback finishCallback) {
         if (transition != mTransition) return false;
 
-        mUnfoldBackgroundController.ensureBackground(startTransaction);
-        startTransaction.apply();
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            animator.clearTasks();
 
-        mFullscreenAnimator.clearTasks();
-        info.getChanges().forEach(change -> {
-            final boolean allowedToAnimate = change.getTaskInfo() != null
-                    && change.getTaskInfo().isVisible()
-                    && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
-                    && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME
-                    && change.getMode() == TRANSIT_CHANGE;
+            info.getChanges().forEach(change -> {
+                if (change.getTaskInfo() != null
+                        && change.getMode() == TRANSIT_CHANGE
+                        && animator.isApplicableTask(change.getTaskInfo())) {
+                    animator.onTaskAppeared(change.getTaskInfo(), change.getLeash());
+                }
+            });
 
-            if (allowedToAnimate) {
-                mFullscreenAnimator.addTask(change.getTaskInfo(), change.getLeash());
+            if (animator.hasActiveTasks()) {
+                animator.prepareStartTransaction(startTransaction);
+                animator.prepareFinishTransaction(finishTransaction);
+                animator.start();
             }
-        });
+        }
 
-        mFullscreenAnimator.resetAllSurfaces(finishTransaction);
-        mUnfoldBackgroundController.removeBackground(finishTransaction);
+        startTransaction.apply();
         mFinishCallback = finishCallback;
         return true;
     }
 
     @Override
     public void onStateChangeProgress(float progress) {
-        final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-        mFullscreenAnimator.applyAnimationProgress(progress, transaction);
-        transaction.apply();
-        mTransactionPool.release(transaction);
+        if (mTransition == null) return;
+
+        SurfaceControl.Transaction transaction = null;
+
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+
+            if (animator.hasActiveTasks()) {
+                if (transaction == null) {
+                    transaction = mTransactionPool.acquire();
+                }
+
+                animator.applyAnimationProgress(progress, transaction);
+            }
+        }
+
+        if (transaction != null) {
+            transaction.apply();
+            mTransactionPool.release(transaction);
+        }
     }
 
     @Override
     public void onStateChangeFinished() {
-        if (mFinishCallback != null) {
-            mFinishCallback.onTransitionFinished(null, null);
-            mFinishCallback = null;
-            mTransition = null;
-            mFullscreenAnimator.clearTasks();
+        if (mFinishCallback == null) return;
+
+        for (int i = 0; i < mAnimators.size(); i++) {
+            final UnfoldTaskAnimator animator = mAnimators.get(i);
+            animator.clearTasks();
+            animator.stop();
         }
+
+        mFinishCallback.onTransitionFinished(null, null);
+        mFinishCallback = null;
+        mTransition = null;
     }
 
     @Nullable
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
index 6ec5512..eab82f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/FullscreenUnfoldTaskAnimator.java
@@ -16,11 +16,14 @@
 
 package com.android.wm.shell.unfold.animation;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.util.MathUtils.lerp;
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import android.animation.RectEvaluator;
 import android.animation.TypeEvaluator;
+import android.annotation.NonNull;
 import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -33,6 +36,8 @@
 
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
 
 /**
  * This helper class contains logic that calculates scaling and cropping parameters
@@ -42,10 +47,10 @@
  *
  * This class is used by
  * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and
- * {@link com.android.wm.shell.fullscreen.FullscreenUnfoldController}. They use independent
+ * {@link UnfoldAnimationController}. They use independent
  * instances of FullscreenUnfoldTaskAnimator.
  */
-public class FullscreenUnfoldTaskAnimator implements
+public class FullscreenUnfoldTaskAnimator implements UnfoldTaskAnimator,
         DisplayInsetsController.OnInsetsChangedListener {
 
     private static final float[] FLOAT_9 = new float[9];
@@ -60,12 +65,15 @@
     private final int mExpandedTaskBarHeight;
     private final float mWindowCornerRadiusPx;
     private final DisplayInsetsController mDisplayInsetsController;
+    private final UnfoldBackgroundController mBackgroundController;
 
     private InsetsSource mTaskbarInsetsSource;
 
     public FullscreenUnfoldTaskAnimator(Context context,
+            @NonNull UnfoldBackgroundController backgroundController,
             DisplayInsetsController displayInsetsController) {
         mDisplayInsetsController = displayInsetsController;
+        mBackgroundController = backgroundController;
         mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
                 com.android.internal.R.dimen.taskbar_frame_height);
         mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
@@ -88,27 +96,32 @@
         return mAnimationContextByTaskId.size() > 0;
     }
 
-    public void addTask(TaskInfo taskInfo, SurfaceControl leash) {
+    @Override
+    public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
         AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource,
                 taskInfo);
         mAnimationContextByTaskId.put(taskInfo.taskId, animationContext);
     }
 
-    public void onTaskInfoChanged(TaskInfo taskInfo) {
+    @Override
+    public void onTaskChanged(TaskInfo taskInfo) {
         AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId);
         if (animationContext != null) {
             animationContext.update(mTaskbarInsetsSource, taskInfo);
         }
     }
 
-    public void removeTask(TaskInfo taskInfo) {
+    @Override
+    public void onTaskVanished(TaskInfo taskInfo) {
         mAnimationContextByTaskId.remove(taskInfo.taskId);
     }
 
+    @Override
     public void clearTasks() {
         mAnimationContextByTaskId.clear();
     }
 
+    @Override
     public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
         final AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
         if (context != null) {
@@ -116,6 +129,7 @@
         }
     }
 
+    @Override
     public void applyAnimationProgress(float progress, Transaction transaction) {
         if (mAnimationContextByTaskId.size() == 0) return;
 
@@ -132,11 +146,29 @@
             transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
                     .setMatrix(context.mLeash, context.mMatrix, FLOAT_9)
                     .setCornerRadius(context.mLeash, mWindowCornerRadiusPx)
-                    .show(context.mLeash)
-            ;
+                    .show(context.mLeash);
         }
     }
 
+    @Override
+    public void prepareStartTransaction(Transaction transaction) {
+        mBackgroundController.ensureBackground(transaction);
+    }
+
+    @Override
+    public void prepareFinishTransaction(Transaction transaction) {
+        mBackgroundController.removeBackground(transaction);
+    }
+
+    @Override
+    public boolean isApplicableTask(TaskInfo taskInfo) {
+        return taskInfo != null && taskInfo.isVisible()
+                && taskInfo.realActivity != null // to filter out parents created by organizer
+                && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                && taskInfo.getActivityType() != ACTIVITY_TYPE_HOME;
+    }
+
+    @Override
     public void resetAllSurfaces(Transaction transaction) {
         for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
             final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
new file mode 100644
index 0000000..6e10ebe
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold.animation;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+
+import android.animation.RectEvaluator;
+import android.animation.TypeEvaluator;
+import android.app.TaskInfo;
+import android.content.Context;
+import android.graphics.Insets;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.splitscreen.SplitScreen;
+import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.unfold.UnfoldAnimationController;
+import com.android.wm.shell.unfold.UnfoldBackgroundController;
+
+import java.util.Optional;
+import java.util.concurrent.Executor;
+
+import dagger.Lazy;
+
+/**
+ * This helper class contains logic that calculates scaling and cropping parameters
+ * for the folding/unfolding animation. As an input it receives TaskInfo objects and
+ * surfaces leashes and as an output it could fill surface transactions with required
+ * transformations.
+ *
+ * This class is used by
+ * {@link com.android.wm.shell.unfold.UnfoldTransitionHandler} and
+ * {@link UnfoldAnimationController}.
+ * They use independent instances of SplitTaskUnfoldAnimator.
+ */
+public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator,
+        DisplayInsetsController.OnInsetsChangedListener, SplitScreenListener {
+
+    private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect());
+    private static final float CROPPING_START_MARGIN_FRACTION = 0.05f;
+
+    private final Executor mExecutor;
+    private final DisplayInsetsController mDisplayInsetsController;
+    private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>();
+    private final int mExpandedTaskBarHeight;
+    private final float mWindowCornerRadiusPx;
+    private final Lazy<Optional<SplitScreenController>> mSplitScreenController;
+    private final UnfoldBackgroundController mUnfoldBackgroundController;
+
+    private final Rect mMainStageBounds = new Rect();
+    private final Rect mSideStageBounds = new Rect();
+    private final Rect mRootStageBounds = new Rect();
+
+    private InsetsSource mTaskbarInsetsSource;
+
+    @SplitPosition
+    private int mMainStagePosition = SPLIT_POSITION_UNDEFINED;
+    @SplitPosition
+    private int mSideStagePosition = SPLIT_POSITION_UNDEFINED;
+
+    public SplitTaskUnfoldAnimator(Context context, Executor executor,
+            Lazy<Optional<SplitScreenController>> splitScreenController,
+            UnfoldBackgroundController unfoldBackgroundController,
+            DisplayInsetsController displayInsetsController) {
+        mDisplayInsetsController = displayInsetsController;
+        mExecutor = executor;
+        mUnfoldBackgroundController = unfoldBackgroundController;
+        mSplitScreenController = splitScreenController;
+        mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.taskbar_frame_height);
+        mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context);
+    }
+
+    /** Initializes the animator, this should be called only once */
+    @Override
+    public void init() {
+        mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this);
+    }
+
+    /**
+     * Starts listening for split-screen changes and gets initial split-screen
+     * layout information through the listener
+     */
+    @Override
+    public void start() {
+        mSplitScreenController.get().get().asSplitScreen()
+                .registerSplitScreenListener(this, mExecutor);
+    }
+
+    /**
+     * Stops listening for the split-screen layout changes
+     */
+    @Override
+    public void stop() {
+        mSplitScreenController.get().get().asSplitScreen()
+                .unregisterSplitScreenListener(this);
+    }
+
+    @Override
+    public void insetsChanged(InsetsState insetsState) {
+        mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR);
+        updateContexts();
+    }
+
+    @Override
+    public void onTaskStageChanged(int taskId, int stage, boolean visible) {
+        final AnimationContext context = mAnimationContextByTaskId.get(taskId);
+        if (context != null) {
+            context.mStageType = stage;
+            context.update();
+        }
+    }
+
+    @Override
+    public void onStagePositionChanged(int stage, int position) {
+        if (stage == STAGE_TYPE_MAIN) {
+            mMainStagePosition = position;
+        } else {
+            mSideStagePosition = position;
+        }
+        updateContexts();
+    }
+
+    @Override
+    public void onSplitBoundsChanged(Rect rootBounds, Rect mainBounds, Rect sideBounds) {
+        mRootStageBounds.set(rootBounds);
+        mMainStageBounds.set(mainBounds);
+        mSideStageBounds.set(sideBounds);
+        updateContexts();
+    }
+
+    private void updateContexts() {
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            context.update();
+        }
+    }
+
+    /**
+     * Register a split task in the animator
+     * @param taskInfo info of the task
+     * @param leash the surface of the task
+     */
+    @Override
+    public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
+        AnimationContext context = new AnimationContext(leash);
+        mAnimationContextByTaskId.put(taskInfo.taskId, context);
+    }
+
+    /**
+     * Unregister the task from the unfold animation
+     * @param taskInfo info of the task
+     */
+    @Override
+    public void onTaskVanished(TaskInfo taskInfo) {
+        mAnimationContextByTaskId.remove(taskInfo.taskId);
+    }
+
+    @Override
+    public boolean isApplicableTask(TaskInfo taskInfo) {
+        return taskInfo.hasParentTask()
+                && taskInfo.isVisible
+                && taskInfo.realActivity != null // to filter out parents created by organizer
+                && taskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW;
+    }
+
+    /**
+     * Clear all registered tasks
+     */
+    @Override
+    public void clearTasks() {
+        mAnimationContextByTaskId.clear();
+    }
+
+    /**
+     * Reset transformations of the task that could have been applied by the animator
+     * @param taskInfo task to reset
+     * @param transaction a transaction to write the changes to
+     */
+    @Override
+    public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
+        AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId);
+        if (context != null) {
+            resetSurface(transaction, context);
+        }
+    }
+
+    /**
+     * Reset all surface transformation that could have been introduced by the animator
+     * @param transaction to write changes to
+     */
+    @Override
+    public void resetAllSurfaces(Transaction transaction) {
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            final AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            resetSurface(transaction, context);
+        }
+    }
+
+    @Override
+    public void applyAnimationProgress(float progress, Transaction transaction) {
+        for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) {
+            AnimationContext context = mAnimationContextByTaskId.valueAt(i);
+            if (context.mStageType == STAGE_TYPE_UNDEFINED) {
+                continue;
+            }
+
+            context.mCurrentCropRect.set(RECT_EVALUATOR
+                    .evaluate(progress, context.mStartCropRect, context.mEndCropRect));
+
+            transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect)
+                    .setCornerRadius(context.mLeash, mWindowCornerRadiusPx);
+        }
+    }
+
+    @Override
+    public void prepareStartTransaction(Transaction transaction) {
+        mUnfoldBackgroundController.ensureBackground(transaction);
+        mSplitScreenController.get().get().updateSplitScreenSurfaces(transaction);
+    }
+
+    @Override
+    public void prepareFinishTransaction(Transaction transaction) {
+        mUnfoldBackgroundController.removeBackground(transaction);
+    }
+
+    /**
+     * @return true if there are tasks to animate
+     */
+    @Override
+    public boolean hasActiveTasks() {
+        return mAnimationContextByTaskId.size() > 0;
+    }
+
+    private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) {
+        transaction
+                .setWindowCrop(context.mLeash, null)
+                .setCornerRadius(context.mLeash, 0.0F);
+    }
+
+    private class AnimationContext {
+        final SurfaceControl mLeash;
+
+        final Rect mStartCropRect = new Rect();
+        final Rect mEndCropRect = new Rect();
+        final Rect mCurrentCropRect = new Rect();
+
+        @SplitScreen.StageType
+        int mStageType = STAGE_TYPE_UNDEFINED;
+
+        private AnimationContext(SurfaceControl leash) {
+            mLeash = leash;
+            update();
+        }
+
+        private void update() {
+            final Rect stageBounds = mStageType == STAGE_TYPE_MAIN
+                    ? mMainStageBounds : mSideStageBounds;
+
+            mStartCropRect.set(stageBounds);
+
+            boolean taskbarExpanded = isTaskbarExpanded();
+            if (taskbarExpanded) {
+                // Only insets the cropping window with taskbar when taskbar is expanded
+                mStartCropRect.inset(mTaskbarInsetsSource.calculateVisibleInsets(mStartCropRect));
+            }
+
+            // Offset to surface coordinates as layout bounds are in screen coordinates
+            mStartCropRect.offsetTo(0, 0);
+
+            mEndCropRect.set(mStartCropRect);
+
+            int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height());
+            int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION);
+
+            // Sides adjacent to split bar or task bar are not be animated.
+            Insets margins;
+            final boolean isLandscape = mRootStageBounds.width() > mRootStageBounds.height();
+            if (isLandscape) { // Left and right splits.
+                margins = getLandscapeMargins(margin, taskbarExpanded);
+            } else { // Top and bottom splits.
+                margins = getPortraitMargins(margin, taskbarExpanded);
+            }
+            mStartCropRect.inset(margins);
+        }
+
+        private Insets getLandscapeMargins(int margin, boolean taskbarExpanded) {
+            int left = margin;
+            int right = margin;
+            int bottom = taskbarExpanded ? 0 : margin; // Taskbar margin.
+            final int splitPosition = mStageType == STAGE_TYPE_MAIN
+                    ? mMainStagePosition : mSideStagePosition;
+            if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                right = 0; // Divider margin.
+            } else {
+                left = 0; // Divider margin.
+            }
+            return Insets.of(left, /* top= */ margin, right, bottom);
+        }
+
+        private Insets getPortraitMargins(int margin, boolean taskbarExpanded) {
+            int bottom = margin;
+            int top = margin;
+            final int splitPosition = mStageType == STAGE_TYPE_MAIN
+                    ? mMainStagePosition : mSideStagePosition;
+            if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+                bottom = 0; // Divider margin.
+            } else { // Bottom split.
+                top = 0; // Divider margin.
+                if (taskbarExpanded) {
+                    bottom = 0; // Taskbar margin.
+                }
+            }
+            return Insets.of(/* left= */ margin, top, /* right= */ margin, bottom);
+        }
+
+        private boolean isTaskbarExpanded() {
+            return mTaskbarInsetsSource != null
+                    && mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java
new file mode 100644
index 0000000..e1e3663
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/UnfoldTaskAnimator.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold.animation;
+
+import android.app.TaskInfo;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+/**
+ * Interface for classes that handle animations of tasks when folding or unfolding
+ * foldable devices.
+ */
+public interface UnfoldTaskAnimator {
+    /**
+     * Initializes the animator, this should be called once in the lifetime of the animator
+     */
+    default void init() {}
+
+    /**
+     * Starts the animator, it might start listening for some events from the system.
+     * Applying animation should be done only when animator is started.
+     * Animator could be started/stopped several times.
+     */
+    default void start() {}
+
+    /**
+     * Stops the animator, it could unsubscribe from system events.
+     */
+    default void stop() {}
+
+    /**
+     * If this method returns true then task updates will be propagated to
+     * the animator using the onTaskAppeared/Changed/Vanished callbacks.
+     * @return true if this task should be animated by this animator
+     */
+    default boolean isApplicableTask(TaskInfo taskInfo) {
+        return false;
+    }
+
+    /**
+     * Called whenever a task applicable to this animator appeared
+     * (isApplicableTask returns true for this task)
+     *
+     * @param taskInfo info of the appeared task
+     * @param leash surface of the task
+     */
+    default void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {}
+
+    /**
+     * Called whenever a task applicable to this animator changed
+     * @param taskInfo info of the changed task
+     */
+    default void onTaskChanged(TaskInfo taskInfo) {}
+
+    /**
+     * Called whenever a task applicable to this animator vanished
+     * @param taskInfo info of the vanished task
+     */
+    default void onTaskVanished(TaskInfo taskInfo) {}
+
+    /**
+     * @return true if there tasks that could be potentially animated
+     */
+    default boolean hasActiveTasks() {
+        return false;
+    }
+
+    /**
+     * Clears all registered tasks in the animator
+     */
+    default void clearTasks() {}
+
+    /**
+     * Apply task surfaces transformations based on the current unfold progress
+     * @param progress unfold transition progress
+     * @param transaction to write changes to
+     */
+    default void applyAnimationProgress(float progress, Transaction transaction) {}
+
+    /**
+     * Apply task surfaces transformations that should be set before starting the animation
+     * @param transaction to write changes to
+     */
+    default void prepareStartTransaction(Transaction transaction) {}
+
+    /**
+     * Apply task surfaces transformations that should be set after finishing the animation
+     * @param transaction to write changes to
+     */
+    default void prepareFinishTransaction(Transaction transaction) {}
+
+    /**
+     * Resets task surface to its initial transformation
+     * @param transaction to write changes to
+     */
+    default void resetSurface(TaskInfo taskInfo, Transaction transaction) {}
+
+    /**
+     * Resets all task surfaces to their initial transformations
+     * @param transaction to write changes to
+     */
+    default void resetAllSurfaces(Transaction transaction) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java
new file mode 100644
index 0000000..4c868305
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldShellTransition.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold.qualifier;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Indicates that this class is used for the shell unfold transition
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UnfoldShellTransition {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java
new file mode 100644
index 0000000..4d2b3e6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/qualifier/UnfoldTransition.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold.qualifier;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Indicates that this class is used for unfold transition implemented
+ * without using Shell transitions
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UnfoldTransition {}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
index 5dacdbf..cba396a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
@@ -71,15 +71,17 @@
     splitLeftTop: Boolean
 ) {
     assertLayers {
+        val dividerRegion = this.last().layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
         this.isInvisible(component)
             .then()
             .invoke("splitAppLayerBoundsBecomesVisible") {
-                val dividerRegion = it.layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
-                it.visibleRegion(component).overlaps(if (splitLeftTop) {
-                    getSplitLeftTopRegion(dividerRegion, rotation)
-                } else {
-                    getSplitRightBottomRegion(dividerRegion, rotation)
-                })
+                it.visibleRegion(component).overlaps(
+                    if (splitLeftTop) {
+                        getSplitLeftTopRegion(dividerRegion, rotation)
+                    } else {
+                        getSplitRightBottomRegion(dividerRegion, rotation)
+                    }
+                )
             }
     }
 }
@@ -91,11 +93,13 @@
 ) {
     assertLayersEnd {
         val dividerRegion = layer(SPLIT_SCREEN_DIVIDER_COMPONENT).visibleRegion.region
-        visibleRegion(component).overlaps(if (splitLeftTop) {
-            getSplitLeftTopRegion(dividerRegion, rotation)
-        } else {
-            getSplitRightBottomRegion(dividerRegion, rotation)
-        })
+        visibleRegion(component).overlaps(
+            if (splitLeftTop) {
+                getSplitLeftTopRegion(dividerRegion, rotation)
+            } else {
+                getSplitRightBottomRegion(dividerRegion, rotation)
+            }
+        )
     }
 }
 
@@ -192,22 +196,30 @@
 fun getPrimaryRegion(dividerRegion: Region, rotation: Int): Region {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
     return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
-        Region.from(0, 0, displayBounds.bounds.right,
-            dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset)
+        Region.from(
+            0, 0, displayBounds.bounds.right,
+            dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
+        )
     } else {
-        Region.from(0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
-            displayBounds.bounds.bottom)
+        Region.from(
+            0, 0, dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
+            displayBounds.bounds.bottom
+        )
     }
 }
 
 fun getSecondaryRegion(dividerRegion: Region, rotation: Int): Region {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
     return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
-        Region.from(0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
-            displayBounds.bounds.right, displayBounds.bounds.bottom)
+        Region.from(
+            0, dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
+            displayBounds.bounds.right, displayBounds.bounds.bottom
+        )
     } else {
-        Region.from(dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0,
-            displayBounds.bounds.right, displayBounds.bounds.bottom)
+        Region.from(
+            dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset, 0,
+            displayBounds.bounds.right, displayBounds.bounds.bottom
+        )
     }
 }
 
@@ -223,10 +235,14 @@
 fun getSplitRightBottomRegion(dividerRegion: Region, rotation: Int): Region {
     val displayBounds = WindowUtils.getDisplayBounds(rotation)
     return if (displayBounds.width > displayBounds.height) {
-        Region.from(dividerRegion.bounds.right, 0, displayBounds.bounds.right,
-                displayBounds.bounds.bottom)
+        Region.from(
+            dividerRegion.bounds.right, 0, displayBounds.bounds.right,
+            displayBounds.bounds.bottom
+        )
     } else {
-        Region.from(0, dividerRegion.bounds.bottom, displayBounds.bounds.right,
-                displayBounds.bounds.bottom)
+        Region.from(
+            0, dividerRegion.bounds.bottom, displayBounds.bounds.right,
+            displayBounds.bounds.bottom
+        )
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
index b902e5d..d4298b8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt
@@ -17,8 +17,18 @@
 package com.android.wm.shell.flicker.helpers
 
 import android.app.Instrumentation
+import android.graphics.Point
+import android.os.SystemClock
+import android.view.InputDevice
+import android.view.MotionEvent
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
 import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.parser.toFlickerComponent
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
 import com.android.wm.shell.flicker.testapp.Components
 
 class SplitScreenHelper(
@@ -30,20 +40,152 @@
     companion object {
         const val TEST_REPETITIONS = 1
         const val TIMEOUT_MS = 3_000L
+        const val DRAG_DURATION_MS = 1_000L
+        const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
+        const val GESTURE_STEP_MS = 16L
+
+        private val notificationScrollerSelector: BySelector
+            get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
+        private val notificationContentSelector: BySelector
+            get() = By.text("Notification content")
 
         fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper =
-            SplitScreenHelper(instrumentation,
+            SplitScreenHelper(
+                instrumentation,
                 Components.SplitScreenActivity.LABEL,
-                Components.SplitScreenActivity.COMPONENT.toFlickerComponent())
+                Components.SplitScreenActivity.COMPONENT.toFlickerComponent()
+            )
 
         fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper =
-            SplitScreenHelper(instrumentation,
+            SplitScreenHelper(
+                instrumentation,
                 Components.SplitScreenSecondaryActivity.LABEL,
-                Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent())
+                Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()
+            )
 
         fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper =
-            SplitScreenHelper(instrumentation,
+            SplitScreenHelper(
+                instrumentation,
                 Components.NonResizeableActivity.LABEL,
-                Components.NonResizeableActivity.COMPONENT.toFlickerComponent())
+                Components.NonResizeableActivity.COMPONENT.toFlickerComponent()
+            )
+
+        fun getSendNotification(instrumentation: Instrumentation): SplitScreenHelper =
+            SplitScreenHelper(
+                instrumentation,
+                Components.SendNotificationActivity.LABEL,
+                Components.SendNotificationActivity.COMPONENT.toFlickerComponent()
+            )
+
+        fun dragFromNotificationToSplit(
+            instrumentation: Instrumentation,
+            device: UiDevice,
+            wmHelper: WindowManagerStateHelper
+        ) {
+            val displayBounds = wmHelper.currentState.layerState
+                .displays.firstOrNull { !it.isVirtual }
+                ?.layerStackSpace
+                ?: error("Display not found")
+
+            // Pull down the notifications
+            device.swipe(
+                displayBounds.centerX(), 5,
+                displayBounds.centerX(), displayBounds.bottom, 20 /* steps */
+            )
+            SystemClock.sleep(TIMEOUT_MS)
+
+            // Find the target notification
+            val notificationScroller = device.wait(
+                Until.findObject(notificationScrollerSelector), TIMEOUT_MS
+            )
+            var notificationContent = notificationScroller.findObject(notificationContentSelector)
+
+            while (notificationContent == null) {
+                device.swipe(
+                    displayBounds.centerX(), displayBounds.centerY(),
+                    displayBounds.centerX(), displayBounds.centerY() - 150, 20 /* steps */
+                )
+                notificationContent = notificationScroller.findObject(notificationContentSelector)
+            }
+
+            // Drag to split
+            var dragStart = notificationContent.visibleCenter
+            var dragMiddle = Point(dragStart.x + 50, dragStart.y)
+            var dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+            val downTime = SystemClock.uptimeMillis()
+
+            touch(
+                instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime,
+                TIMEOUT_MS, dragStart
+            )
+            // It needs a horizontal movement to trigger the drag
+            touchMove(
+                instrumentation, downTime, SystemClock.uptimeMillis(),
+                DRAG_DURATION_MS, dragStart, dragMiddle
+            )
+            touchMove(
+                instrumentation, downTime, SystemClock.uptimeMillis(),
+                DRAG_DURATION_MS, dragMiddle, dragEnd
+            )
+            // Wait for a while to start splitting
+            SystemClock.sleep(TIMEOUT_MS)
+            touch(
+                instrumentation, MotionEvent.ACTION_UP, downTime, SystemClock.uptimeMillis(),
+                GESTURE_STEP_MS, dragEnd
+            )
+            SystemClock.sleep(TIMEOUT_MS)
+        }
+
+        fun touch(
+            instrumentation: Instrumentation,
+            action: Int,
+            downTime: Long,
+            eventTime: Long,
+            duration: Long,
+            point: Point
+        ) {
+            val motionEvent = MotionEvent.obtain(
+                downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0
+            )
+            motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
+            instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
+            motionEvent.recycle()
+            SystemClock.sleep(duration)
+        }
+
+        fun touchMove(
+            instrumentation: Instrumentation,
+            downTime: Long,
+            eventTime: Long,
+            duration: Long,
+            from: Point,
+            to: Point
+        ) {
+            val steps: Long = duration / GESTURE_STEP_MS
+            var currentTime = eventTime
+            var currentX = from.x.toFloat()
+            var currentY = from.y.toFloat()
+            val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
+            val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
+
+            for (i in 1..steps) {
+                val motionMove = MotionEvent.obtain(
+                    downTime, currentTime, MotionEvent.ACTION_MOVE, currentX, currentY, 0
+                )
+                motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
+                instrumentation.uiAutomation.injectInputEvent(motionMove, true)
+                motionMove.recycle()
+
+                currentTime += GESTURE_STEP_MS
+                if (i == steps - 1) {
+                    currentX = to.x.toFloat()
+                    currentY = to.y.toFloat()
+                } else {
+                    currentX += stepX
+                    currentY += stepY
+                }
+                SystemClock.sleep(GESTURE_STEP_MS)
+            }
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
new file mode 100644
index 0000000..7323d99
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.RequiresDevice
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.Until
+import com.android.server.wm.flicker.FlickerParametersRunnerFactory
+import com.android.server.wm.flicker.FlickerTestParameter
+import com.android.server.wm.flicker.FlickerTestParameterFactory
+import com.android.server.wm.flicker.annotation.Group1
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.helpers.SplitScreenHelper
+import com.android.wm.shell.flicker.layerBecomesVisible
+import com.android.wm.shell.flicker.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
+import org.junit.Assume
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test enter split screen by dragging app icon from notification.
+ * This test is only for large screen devices.
+ *
+ * To run this test: `atest WMShellFlickerTests:EnterSplitScreenByDragFromNotification`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Group1
+class EnterSplitScreenByDragFromNotification(
+    testSpec: FlickerTestParameter
+) : SplitScreenBase(testSpec) {
+
+    private val sendNotificationApp = SplitScreenHelper.getSendNotification(instrumentation)
+
+    @Before
+    fun before() {
+        Assume.assumeTrue(taplInstrumentation.isTablet)
+    }
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            super.transition(this)
+            setup {
+                eachRun {
+                    // Send a notification
+                    sendNotificationApp.launchViaIntent(wmHelper)
+                    val sendNotification = device.wait(
+                        Until.findObject(By.text("Send Notification")),
+                        SplitScreenHelper.TIMEOUT_MS
+                    )
+                    sendNotification?.click() ?: error("Send notification button not found")
+
+                    taplInstrumentation.goHome()
+                    primaryApp.launchViaIntent(wmHelper)
+                }
+            }
+            transitions {
+                SplitScreenHelper.dragFromNotificationToSplit(instrumentation, device, wmHelper)
+            }
+            teardown {
+                eachRun {
+                    sendNotificationApp.exit(wmHelper)
+                }
+            }
+        }
+
+    @Presubmit
+    @Test
+    fun dividerBecomesVisible() = testSpec.splitScreenDividerBecomesVisible()
+
+    @Presubmit
+    @Test
+    fun primaryAppLayerIsVisibleAtEnd() = testSpec.layerIsVisibleAtEnd(primaryApp.component)
+
+    @Presubmit
+    @Test
+    fun secondaryAppLayerBecomesVisible() =
+        testSpec.layerBecomesVisible(sendNotificationApp.component)
+
+    @Presubmit
+    @Test
+    fun primaryAppBoundsIsVisibleAtEnd() = testSpec.splitAppLayerBoundsIsVisibleAtEnd(
+        testSpec.endRotation, primaryApp.component, false /* splitLeftTop */
+    )
+
+    @Presubmit
+    @Test
+    fun secondaryAppBoundsBecomesVisible() = testSpec.splitAppLayerBoundsBecomesVisible(
+        testSpec.endRotation, sendNotificationApp.component, true /* splitLeftTop */
+    )
+
+    @Presubmit
+    @Test
+    fun primaryAppWindowIsVisibleAtEnd() = testSpec.appWindowIsVisibleAtEnd(primaryApp.component)
+
+    @Presubmit
+    @Test
+    fun secondaryAppWindowIsVisibleAtEnd() =
+        testSpec.appWindowIsVisibleAtEnd(sendNotificationApp.component)
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): List<FlickerTestParameter> {
+            return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests(
+                repetitions = SplitScreenHelper.TEST_REPETITIONS,
+                // TODO(b/176061063):The 3 buttons of nav bar do not exist in the hierarchy.
+                supportedNavigationModes =
+                    listOf(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY)
+            )
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
index bd98585..bc0b0b6 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml
@@ -92,6 +92,17 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".SendNotificationActivity"
+                  android:taskAffinity="com.android.wm.shell.flicker.testapp.SendNotificationActivity"
+                  android:theme="@style/CutoutShortEdges"
+                  android:label="SendNotificationApp"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+
         <activity android:name=".NonResizeableActivity"
                   android:resizeableActivity="false"
                   android:taskAffinity="com.android.wm.shell.flicker.testapp.NonResizeableActivity"
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
new file mode 100644
index 0000000..8d59b56
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_notification.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@android:color/black">
+
+        <Button
+            android:id="@+id/button_send_notification"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerHorizontal="true"
+            android:layout_centerVertical="true"
+            android:text="Send Notification" />
+</RelativeLayout>
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
index 0ed59bd..a2b580d 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java
@@ -88,6 +88,12 @@
                 PACKAGE_NAME + ".SplitScreenSecondaryActivity");
     }
 
+    public static class SendNotificationActivity {
+        public static final String LABEL = "SendNotificationApp";
+        public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
+                PACKAGE_NAME + ".SendNotificationActivity");
+    }
+
     public static class LaunchBubbleActivity {
         public static final String LABEL = "LaunchBubbleApp";
         public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME,
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
new file mode 100644
index 0000000..8020ef2
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/SendNotificationActivity.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.testapp;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+public class SendNotificationActivity extends Activity {
+    private NotificationManager mNotificationManager;
+    private String mChannelId = "Channel id";
+    private String mChannelName = "Channel name";
+    private NotificationChannel mChannel;
+    private int mNotifyId = 0;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_notification);
+        findViewById(R.id.button_send_notification).setOnClickListener(this::sendNotification);
+
+        mChannel = new NotificationChannel(mChannelId, mChannelName,
+                NotificationManager.IMPORTANCE_DEFAULT);
+        mNotificationManager = getSystemService(NotificationManager.class);
+        mNotificationManager.createNotificationChannel(mChannel);
+    }
+
+    private void sendNotification(View v) {
+        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
+                new Intent(this, SendNotificationActivity.class),
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+        Notification notification = new Notification.Builder(this, mChannelId)
+                .setContentTitle("Notification App")
+                .setContentText("Notification content")
+                .setWhen(System.currentTimeMillis())
+                .setSmallIcon(R.drawable.ic_message)
+                .setContentIntent(pendingIntent)
+                .build();
+
+        mNotificationManager.notify(mNotifyId, notification);
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index a6caefe..0b53c40 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -133,7 +133,7 @@
                     .when(mTaskOrganizerController).registerTaskOrganizer(any());
         } catch (RemoteException e) {}
         mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
-                mCompatUI, Optional.empty()));
+                mCompatUI, Optional.empty(), Optional.empty()));
     }
 
     @Test
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
deleted file mode 100644
index 4523e2c..0000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
+++ /dev/null
@@ -1,166 +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.wm.shell.fullscreen;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-
-import static org.junit.Assume.assumeFalse;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.WindowConfiguration;
-import android.content.res.Configuration;
-import android.graphics.Point;
-import android.os.SystemProperties;
-import android.view.SurfaceControl;
-
-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;
-import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Optional;
-
-@SmallTest
-public class FullscreenTaskListenerTest {
-    private static final boolean ENABLE_SHELL_TRANSITIONS =
-            SystemProperties.getBoolean("persist.wm.debug.shell_transit", false);
-
-    @Mock
-    private SyncTransactionQueue mSyncQueue;
-    @Mock
-    private FullscreenUnfoldController mUnfoldController;
-    @Mock
-    private RecentTasksController mRecentTasksController;
-    @Mock
-    private SurfaceControl mSurfaceControl;
-
-    private Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
-
-    private FullscreenTaskListener mListener;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mFullscreenUnfoldController = Optional.of(mUnfoldController);
-        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController,
-                Optional.empty());
-    }
-
-    @Test
-    public void testAnimatableTaskAppeared_notifiesUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0);
-
-        mListener.onTaskAppeared(info, mSurfaceControl);
-
-        verify(mUnfoldController).onTaskAppeared(eq(info), any());
-    }
-
-    @Test
-    public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0);
-        RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1);
-
-        mListener.onTaskAppeared(animatable1, mSurfaceControl);
-        mListener.onTaskAppeared(animatable2, mSurfaceControl);
-
-        InOrder order = inOrder(mUnfoldController);
-        order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any());
-        order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any());
-    }
-
-    @Test
-    public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
-
-        mListener.onTaskAppeared(info, mSurfaceControl);
-
-        verifyNoMoreInteractions(mUnfoldController);
-    }
-
-    @Test
-    public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
-        mListener.onTaskAppeared(info, mSurfaceControl);
-
-        mListener.onTaskInfoChanged(info);
-
-        verifyNoMoreInteractions(mUnfoldController);
-    }
-
-    @Test
-    public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0);
-        mListener.onTaskAppeared(info, mSurfaceControl);
-
-        mListener.onTaskVanished(info);
-
-        verifyNoMoreInteractions(mUnfoldController);
-    }
-
-    @Test
-    public void testAnimatableTaskBecameInactive_notifiesUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0);
-        mListener.onTaskAppeared(animatableTask, mSurfaceControl);
-        RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0);
-
-        mListener.onTaskInfoChanged(notAnimatableTask);
-
-        verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask));
-    }
-
-    @Test
-    public void testAnimatableTaskVanished_notifiesUnfoldController() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0);
-        mListener.onTaskAppeared(taskInfo, mSurfaceControl);
-
-        mListener.onTaskVanished(taskInfo);
-
-        verify(mUnfoldController).onTaskVanished(eq(taskInfo));
-    }
-
-    private RunningTaskInfo createTaskInfo(boolean visible, int taskId) {
-        final RunningTaskInfo info = spy(new RunningTaskInfo());
-        info.isVisible = visible;
-        info.positionInParent = new Point();
-        when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
-        final Configuration configuration = new Configuration();
-        configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
-        when(info.getConfiguration()).thenReturn(configuration);
-        info.taskId = taskId;
-        return info;
-    }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
index 440a6f8..1eadeed 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
@@ -88,7 +88,7 @@
         // NOTE: KidsModeTaskOrganizer should have a null CompatUIController.
         mOrganizer = spy(new KidsModeTaskOrganizer(mTaskOrganizerController, mTestExecutor,
                 mHandler, mContext, mSyncTransactionQueue, mDisplayController,
-                mDisplayInsetsController, Optional.empty(), mObserver));
+                mDisplayInsetsController, Optional.empty(), Optional.empty(), mObserver));
         mOrganizer.initialize(mStartingWindowController);
         doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction();
         doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index d515d43..2b4d1a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -80,7 +80,7 @@
         mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener,
                 mMainExecutor));
         mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext,
-                null /* sizeCompatUI */, Optional.of(mRecentTasksController));
+                null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
index 0639ad5..68cb57c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java
@@ -61,7 +61,7 @@
         MockitoAnnotations.initMocks(this);
         mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
         mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
-                mSyncQueue, mSurfaceSession, mIconProvider, null);
+                mSyncQueue, mSurfaceSession, mIconProvider);
         mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
index a31aa58..3b42a48 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java
@@ -66,7 +66,7 @@
         MockitoAnnotations.initMocks(this);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks,
-                mSyncQueue, mSurfaceSession, mIconProvider, null);
+                mSyncQueue, mSurfaceSession, mIconProvider);
         mSideStage.onTaskAppeared(mRootTask, mRootLeash);
     }
 
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 eb9d3a1..a67853c 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
@@ -40,8 +40,6 @@
 
 import java.util.Optional;
 
-import javax.inject.Provider;
-
 public class SplitTestUtils {
 
     static SplitLayout createMockSplitLayout() {
@@ -74,12 +72,10 @@
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
                 SplitscreenEventLogger logger, ShellExecutor mainExecutor,
-                Optional<RecentTasksController> recentTasks,
-                Provider<Optional<StageTaskUnfoldController>> unfoldController) {
+                Optional<RecentTasksController> recentTasks) {
             super(context, displayId, syncQueue, taskOrganizer, mainStage,
                     sideStage, displayController, imeController, insetsController, splitLayout,
-                    transitions, transactionPool, logger, mainExecutor, recentTasks,
-                    unfoldController);
+                    transitions, transactionPool, logger, mainExecutor, recentTasks);
 
             // Prepare root task for testing.
             mRootTask = new TestRunningTaskInfoBuilder().build();
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 b526904..304ca66 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
@@ -118,16 +118,16 @@
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider, null);
+                mIconProvider);
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
                 StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
-                mIconProvider, null);
+                mIconProvider);
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
-                mTransactionPool, mLogger, mMainExecutor, Optional.empty(), Optional::empty);
+                mTransactionPool, mLogger, mMainExecutor, 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 42d998f..af2c495 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
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -58,6 +59,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.splitscreen.SplitScreen.SplitScreenListener;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -68,8 +70,6 @@
 
 import java.util.Optional;
 
-import javax.inject.Provider;
-
 /**
  * Tests for {@link StageCoordinator}
  */
@@ -85,10 +85,6 @@
     @Mock
     private SideStage mSideStage;
     @Mock
-    private StageTaskUnfoldController mMainUnfoldController;
-    @Mock
-    private StageTaskUnfoldController mSideUnfoldController;
-    @Mock
     private SplitLayout mSplitLayout;
     @Mock
     private DisplayController mDisplayController;
@@ -107,6 +103,7 @@
 
     private final Rect mBounds1 = new Rect(10, 20, 30, 40);
     private final Rect mBounds2 = new Rect(5, 10, 15, 20);
+    private final Rect mRootBounds = new Rect(0, 0, 45, 60);
 
     private SurfaceSession mSurfaceSession = new SurfaceSession();
     private SurfaceControl mRootLeash;
@@ -119,11 +116,12 @@
         mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                 mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController,
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mLogger,
-                mMainExecutor, Optional.empty(), new UnfoldControllerProvider()));
+                mMainExecutor, Optional.empty()));
         doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt());
 
         when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
         when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
+        when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds);
         when(mSplitLayout.isLandscape()).thenReturn(false);
 
         mRootTask = new TestRunningTaskInfoBuilder().build();
@@ -168,13 +166,6 @@
     }
 
     @Test
-    public void testRootTaskAppeared_initializesUnfoldControllers() {
-        verify(mMainUnfoldController).init();
-        verify(mSideUnfoldController).init();
-        verify(mStageCoordinator).onRootTaskAppeared();
-    }
-
-    @Test
     public void testRootTaskInfoChanged_updatesSplitLayout() {
         mStageCoordinator.onTaskInfoChanged(mRootTask);
 
@@ -184,26 +175,25 @@
     @Test
     public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() {
         mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null);
-        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+        final SplitScreenListener listener = mock(SplitScreenListener.class);
+        mStageCoordinator.registerSplitScreenListener(listener);
+        clearInvocations(listener);
 
         mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
 
-        verify(mMainUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                false);
-        verify(mSideUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT, false);
+        verify(listener).onSplitBoundsChanged(mRootBounds, mBounds2, mBounds1);
     }
 
     @Test
     public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() {
         mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null);
-        clearInvocations(mMainUnfoldController, mSideUnfoldController);
+        final SplitScreenListener listener = mock(SplitScreenListener.class);
+        mStageCoordinator.registerSplitScreenListener(listener);
+        clearInvocations(listener);
 
         mStageCoordinator.onLayoutSizeChanged(mSplitLayout);
 
-        verify(mMainUnfoldController).onLayoutChanged(mBounds1, SPLIT_POSITION_TOP_OR_LEFT,
-                false);
-        verify(mSideUnfoldController).onLayoutChanged(mBounds2, SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                false);
+        verify(listener).onSplitBoundsChanged(mRootBounds, mBounds1, mBounds2);
     }
 
     @Test
@@ -314,20 +304,4 @@
 
         verify(mSplitLayout).applySurfaceChanges(any(), any(), any(), any(), any(), eq(false));
     }
-
-    private class UnfoldControllerProvider implements
-            Provider<Optional<StageTaskUnfoldController>> {
-
-        private boolean isMain = true;
-
-        @Override
-        public Optional<StageTaskUnfoldController> get() {
-            if (isMain) {
-                isMain = false;
-                return Optional.of(mMainUnfoldController);
-            } else {
-                return Optional.of(mSideUnfoldController);
-            }
-        }
-    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 157c30b..5ee8bf3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -25,7 +25,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -72,8 +71,6 @@
     private SyncTransactionQueue mSyncQueue;
     @Mock
     private IconProvider mIconProvider;
-    @Mock
-    private StageTaskUnfoldController mStageTaskUnfoldController;
     @Captor
     private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
     private SurfaceSession mSurfaceSession = new SurfaceSession();
@@ -92,8 +89,7 @@
                 mCallbacks,
                 mSyncQueue,
                 mSurfaceSession,
-                mIconProvider,
-                mStageTaskUnfoldController);
+                mIconProvider);
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
         mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
@@ -130,30 +126,6 @@
         verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true));
     }
 
-    @Test
-    public void testTaskAppeared_notifiesUnfoldListener() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        final ActivityManager.RunningTaskInfo task =
-                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
-
-        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
-
-        verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl));
-    }
-
-    @Test
-    public void testTaskVanished_notifiesUnfoldListener() {
-        assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        final ActivityManager.RunningTaskInfo task =
-                new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build();
-        mStageTaskListener.onTaskAppeared(task, mSurfaceControl);
-        clearInvocations(mStageTaskUnfoldController);
-
-        mStageTaskListener.onTaskVanished(task);
-
-        verify(mStageTaskUnfoldController).onTaskVanished(eq(task));
-    }
-
     @Test(expected = IllegalArgumentException.class)
     public void testUnknownTaskVanished() {
         final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
new file mode 100644
index 0000000..7982089
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.unfold;
+
+import static com.android.wm.shell.unfold.UnfoldAnimationControllerTest.TestUnfoldTaskAnimator.UNSET;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.TaskInfo;
+import android.testing.AndroidTestingRunner;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Predicate;
+
+/**
+ * Tests for {@link UnfoldAnimationController}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:UnfoldAnimationControllerTest
+ */
+@RunWith(AndroidTestingRunner.class)
+public class UnfoldAnimationControllerTest extends ShellTestCase {
+
+    @Mock
+    private TransactionPool mTransactionPool;
+    @Mock
+    private UnfoldTransitionHandler mUnfoldTransitionHandler;
+    @Mock
+    private SurfaceControl mLeash;
+
+    private UnfoldAnimationController mUnfoldAnimationController;
+
+    private final TestShellUnfoldProgressProvider mProgressProvider =
+            new TestShellUnfoldProgressProvider();
+    private final TestShellExecutor mShellExecutor = new TestShellExecutor();
+
+    private final TestUnfoldTaskAnimator mTaskAnimator1 = new TestUnfoldTaskAnimator();
+    private final TestUnfoldTaskAnimator mTaskAnimator2 = new TestUnfoldTaskAnimator();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mTransactionPool.acquire()).thenReturn(mock(SurfaceControl.Transaction.class));
+
+        final List<UnfoldTaskAnimator> animators = new ArrayList<>();
+        animators.add(mTaskAnimator1);
+        animators.add(mTaskAnimator2);
+        mUnfoldAnimationController = new UnfoldAnimationController(
+                mTransactionPool,
+                mProgressProvider,
+                animators,
+                () -> Optional.of(mUnfoldTransitionHandler),
+                mShellExecutor
+        );
+    }
+
+    @Test
+    public void testAppearedMatchingTask_appliesUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+    }
+
+    @Test
+    public void testAppearedMatchingTaskTwoDifferentAnimators_appliesUnfoldProgressToBoth() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 1);
+        mTaskAnimator2.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(1).build();
+        RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo1, mLeash);
+        mUnfoldAnimationController.onTaskAppeared(taskInfo2, mLeash);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+        assertThat(mTaskAnimator2.mLastAppliedProgress).isEqualTo(0.5f);
+    }
+
+    @Test
+    public void testAppearedNonMatchingTask_doesNotApplyUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(0).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+    }
+
+    @Test
+    public void testAppearedAndChangedToNonMatchingTask_doesNotApplyUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(0);
+        mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+    }
+
+    @Test
+    public void testAppearedAndChangedToNonMatchingTaskAndBack_appliesUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(0);
+        mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(2);
+        mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+    }
+
+    @Test
+    public void testAppearedNonMatchingTaskAndChangedToMatching_appliesUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(0).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        taskInfo.configuration.windowConfiguration.setWindowingMode(2);
+        mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+    }
+
+    @Test
+    public void testAppearedMatchingTaskAndChanged_appliesUnfoldProgress() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        mUnfoldAnimationController.onTaskInfoChanged(taskInfo);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(0.5f);
+    }
+
+    @Test
+    public void testShellTransitionRunning_doesNotApplyUnfoldProgress() {
+        when(mUnfoldTransitionHandler.willHandleTransition()).thenReturn(true);
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+
+        mUnfoldAnimationController.onStateChangeProgress(0.5f);
+        assertThat(mTaskAnimator1.mLastAppliedProgress).isEqualTo(UNSET);
+    }
+
+    @Test
+    public void testApplicableTaskDisappeared_resetsSurface() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(2).build();
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+
+        mUnfoldAnimationController.onTaskVanished(taskInfo);
+
+        assertThat(mTaskAnimator1.mResetTasks).contains(taskInfo.taskId);
+    }
+
+    @Test
+    public void testNonApplicableTaskAppearedDisappeared_doesNotResetSurface() {
+        mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
+        RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+                .setWindowingMode(0).build();
+
+        mUnfoldAnimationController.onTaskAppeared(taskInfo, mLeash);
+        mUnfoldAnimationController.onTaskVanished(taskInfo);
+
+        assertThat(mTaskAnimator1.mResetTasks).doesNotContain(taskInfo.taskId);
+    }
+
+    @Test
+    public void testInit_initsAndStartsAnimators() {
+        mUnfoldAnimationController.init();
+
+        assertThat(mTaskAnimator1.mInitialized).isTrue();
+        assertThat(mTaskAnimator1.mStarted).isTrue();
+    }
+
+    private static class TestShellUnfoldProgressProvider implements ShellUnfoldProgressProvider,
+            ShellUnfoldProgressProvider.UnfoldListener {
+
+        private final List<UnfoldListener> mListeners = new ArrayList<>();
+
+        @Override
+        public void addListener(Executor executor, UnfoldListener listener) {
+            mListeners.add(listener);
+        }
+
+        @Override
+        public void onStateChangeStarted() {
+            mListeners.forEach(UnfoldListener::onStateChangeStarted);
+        }
+
+        @Override
+        public void onStateChangeProgress(float progress) {
+            mListeners.forEach(unfoldListener -> unfoldListener.onStateChangeProgress(progress));
+        }
+
+        @Override
+        public void onStateChangeFinished() {
+            mListeners.forEach(UnfoldListener::onStateChangeFinished);
+        }
+    }
+
+    public static class TestUnfoldTaskAnimator implements UnfoldTaskAnimator {
+
+        public static final float UNSET = -1f;
+        private Predicate<TaskInfo> mTaskMatcher = (info) -> false;
+
+        Map<Integer, TaskInfo> mTasksMap = new HashMap<>();
+        Set<Integer> mResetTasks = new HashSet<>();
+
+        boolean mInitialized = false;
+        boolean mStarted = false;
+        float mLastAppliedProgress = UNSET;
+
+        @Override
+        public void init() {
+            mInitialized = true;
+        }
+
+        @Override
+        public void start() {
+            mStarted = true;
+        }
+
+        @Override
+        public void stop() {
+            mStarted = false;
+        }
+
+        @Override
+        public boolean isApplicableTask(TaskInfo taskInfo) {
+            return mTaskMatcher.test(taskInfo);
+        }
+
+        @Override
+        public void applyAnimationProgress(float progress, Transaction transaction) {
+            mLastAppliedProgress = progress;
+        }
+
+        public void setTaskMatcher(Predicate<TaskInfo> taskMatcher) {
+            mTaskMatcher = taskMatcher;
+        }
+
+        @Override
+        public void onTaskAppeared(TaskInfo taskInfo, SurfaceControl leash) {
+            mTasksMap.put(taskInfo.taskId, taskInfo);
+        }
+
+        @Override
+        public void onTaskVanished(TaskInfo taskInfo) {
+            mTasksMap.remove(taskInfo.taskId);
+        }
+
+        @Override
+        public void onTaskChanged(TaskInfo taskInfo) {
+            mTasksMap.put(taskInfo.taskId, taskInfo);
+        }
+
+        @Override
+        public void resetSurface(TaskInfo taskInfo, Transaction transaction) {
+            mResetTasks.add(taskInfo.taskId);
+        }
+
+        @Override
+        public void resetAllSurfaces(Transaction transaction) {
+            mTasksMap.values().forEach((t) -> mResetTasks.add(t.taskId));
+        }
+
+        @Override
+        public boolean hasActiveTasks() {
+            return mTasksMap.size() > 0;
+        }
+
+        public List<TaskInfo> getCurrentTasks() {
+            return new ArrayList<>(mTasksMap.values());
+        }
+    }
+}
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 490a3f5..ce0dda3 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -713,7 +713,7 @@
 
         <service
             android:name=".dreams.DreamOverlayService"
-            android:enabled="@bool/config_dreamOverlayServiceEnabled"
+            android:enabled="false"
             android:exported="true" />
 
         <activity android:name=".keyguard.WorkLockActivity"
diff --git a/packages/SystemUI/res/layout/large_screen_shade_header.xml b/packages/SystemUI/res/layout/large_screen_shade_header.xml
index 250eabd..3029a27 100644
--- a/packages/SystemUI/res/layout/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/layout/large_screen_shade_header.xml
@@ -22,7 +22,7 @@
     android:minHeight="@dimen/large_screen_shade_header_min_height"
     android:clickable="false"
     android:focusable="true"
-    android:paddingLeft="@dimen/qs_panel_padding"
+    android:paddingLeft="@dimen/large_screen_shade_header_left_padding"
     android:paddingRight="@dimen/qs_panel_padding"
     android:visibility="gone"
     android:theme="@style/Theme.SystemUI.QuickSettings.Header">
diff --git a/packages/SystemUI/res/layout/qs_tile_label.xml b/packages/SystemUI/res/layout/qs_tile_label.xml
index 77523ec9..c124aea 100644
--- a/packages/SystemUI/res/layout/qs_tile_label.xml
+++ b/packages/SystemUI/res/layout/qs_tile_label.xml
@@ -28,7 +28,7 @@
     android:importantForAccessibility="no"
     android:layout_gravity="center_vertical | start">
 
-    <com.android.systemui.util.SafeMarqueeTextView
+    <com.android.systemui.util.DelayableMarqueeTextView
         android:id="@+id/tile_label"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -41,7 +41,7 @@
         android:importantForAccessibility="no"
         android:textAppearance="@style/TextAppearance.QS.TileLabel"/>
 
-    <com.android.systemui.util.SafeMarqueeTextView
+    <com.android.systemui.util.DelayableMarqueeTextView
         android:id="@+id/app_label"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index 4de7bb7..a0bf072 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -25,6 +25,7 @@
     <dimen name="keyguard_status_view_bottom_margin">80dp</dimen>
     <dimen name="bouncer_user_switcher_y_trans">90dp</dimen>
 
+    <dimen name="large_screen_shade_header_left_padding">24dp</dimen>
     <dimen name="qqs_layout_padding_bottom">40dp</dimen>
 
     <dimen name="notification_panel_margin_horizontal">80dp</dimen>
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 3228c3c..70a72ad 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -193,5 +193,9 @@
     <declare-styleable name="DreamOverlayDotImageView">
         <attr name="dotColor" format="color" />
     </declare-styleable>
+
+    <declare-styleable name="DelayableMarqueeTextView">
+        <attr name="marqueeDelay" format="integer" />
+    </declare-styleable>
 </resources>
 
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9e6b119..f72a657 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -689,9 +689,6 @@
     <!-- Flag to enable privacy dot views, it shall be true for normal case -->
     <bool name="config_enablePrivacyDot">true</bool>
 
-    <!-- Flag to enable dream overlay service and its registration -->
-    <bool name="config_dreamOverlayServiceEnabled">false</bool>
-
     <!-- Class for the communal source connector to be used -->
     <string name="config_communalSourceConnector" translatable="false"></string>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 95b6128..08138e4 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -407,6 +407,7 @@
     <!-- Height of status bar in split shade mode - visible only on large screens -->
     <dimen name="large_screen_shade_header_height">@*android:dimen/quick_qs_offset_height</dimen>
     <dimen name="large_screen_shade_header_min_height">@dimen/qs_header_row_min_height</dimen>
+    <dimen name="large_screen_shade_header_left_padding">@dimen/qs_horizontal_margin</dimen>
 
     <!-- The top margin of the panel that holds the list of notifications.
          On phones it's always 0dp but it's overridden in Car UI
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index 3c4c1b6..487e1a4 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -23,7 +23,6 @@
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.icu.text.NumberFormat;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
@@ -144,7 +143,6 @@
 
     @Override
     protected void onViewAttached() {
-        Log.d(TAG, "onViewAttached mView=" + mView);
         updateLocale();
         mBroadcastDispatcher.registerReceiver(mLocaleBroadcastReceiver,
                 new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
@@ -162,7 +160,6 @@
 
     @Override
     protected void onViewDetached() {
-        Log.d(TAG, "onViewDetached mView=" + mView);
         mBroadcastDispatcher.unregisterReceiver(mLocaleBroadcastReceiver);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
         mBatteryController.removeCallback(mBatteryCallback);
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
index 19d39d5..e22386e 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockView.kt
@@ -25,7 +25,6 @@
 import android.text.TextUtils
 import android.text.format.DateFormat
 import android.util.AttributeSet
-import android.util.Log
 import android.widget.TextView
 import com.android.systemui.R
 import com.android.systemui.animation.Interpolators
@@ -133,22 +132,6 @@
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
-            Log.d(
-                tag, "refreshTime this=$this" +
-                        " currTimeContextDesc=$contentDescription" +
-                        " measuredHeight=$measuredHeight" +
-                        " lastMeasureCall=$lastMeasureCall" +
-                        " isSingleLineInternal=$isSingleLineInternal"
-            )
-        } else {
-            Log.d(
-                tag, "refreshTime (skipped due to unchanged text)" +
-                        " this=$this" +
-                        " currTimeContextDesc=$contentDescription" +
-                        " measuredHeight=$measuredHeight" +
-                        " lastMeasureCall=$lastMeasureCall" +
-                        " isSingleLineInternal=$isSingleLineInternal"
-            )
         }
     }
 
@@ -169,20 +152,10 @@
         } else {
             animator.updateLayout(layout)
         }
-        Log.v(tag, "onMeasure this=$this" +
-                " currTimeContextDesc=$contentDescription" +
-                " heightMeasureSpecMode=${MeasureSpec.getMode(heightMeasureSpec)}" +
-                " heightMeasureSpecSize=${MeasureSpec.getSize(heightMeasureSpec)}" +
-                " measuredWidth=$measuredWidth" +
-                " measuredHeight=$measuredHeight" +
-                " isSingleLineInternal=$isSingleLineInternal")
     }
 
     override fun onDraw(canvas: Canvas) {
         // intentionally doesn't call super.onDraw here or else the text will be rendered twice
-        Log.d(tag, "onDraw this=$this" +
-                " currTimeContextDesc=$contentDescription" +
-                " isSingleLineInternal=$isSingleLineInternal")
         textAnimator?.draw(canvas)
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index bd5bceb..40edfe5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -304,7 +304,7 @@
         super.onLayout(changed, l, t, r, b);
 
         if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
-            updateClockViews(mDisplayedClockSize == LARGE, /* animate */ true);
+            post(() -> updateClockViews(mDisplayedClockSize == LARGE, /* animate */ true));
         }
 
         mChildrenAreLaidOut = true;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index ea14b64..5c9dd5e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -237,23 +237,12 @@
         mStatusArea = mView.findViewById(R.id.keyguard_status_area);
 
         if (mSmartspaceController.isEnabled()) {
-            mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
             View ksv = mView.findViewById(R.id.keyguard_slice_view);
             int ksvIndex = mStatusArea.indexOfChild(ksv);
             ksv.setVisibility(View.GONE);
 
-            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
-                    MATCH_PARENT, WRAP_CONTENT);
-
-            mStatusArea.addView(mSmartspaceView, ksvIndex, lp);
-            int startPadding = getContext().getResources()
-                    .getDimensionPixelSize(R.dimen.below_clock_padding_start);
-            int endPadding = getContext().getResources()
-                    .getDimensionPixelSize(R.dimen.below_clock_padding_end);
-            mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0);
-
+            addSmartspaceView(ksvIndex);
             updateClockLayout();
-            mKeyguardUnlockAnimationController.setLockscreenSmartspace(mSmartspaceView);
         }
 
         mSecureSettings.registerContentObserverForUser(
@@ -287,6 +276,30 @@
                 mKeyguardUnlockAnimationListener);
     }
 
+    void onLocaleListChanged() {
+        if (mSmartspaceController.isEnabled()) {
+            int index = mStatusArea.indexOfChild(mSmartspaceView);
+            if (index >= 0) {
+                mStatusArea.removeView(mSmartspaceView);
+                addSmartspaceView(index);
+            }
+        }
+    }
+
+    private void addSmartspaceView(int index) {
+        mSmartspaceView = mSmartspaceController.buildAndConnectView(mView);
+        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                MATCH_PARENT, WRAP_CONTENT);
+        mStatusArea.addView(mSmartspaceView, index, lp);
+        int startPadding = getContext().getResources().getDimensionPixelSize(
+                R.dimen.below_clock_padding_start);
+        int endPadding = getContext().getResources().getDimensionPixelSize(
+                R.dimen.below_clock_padding_end);
+        mSmartspaceView.setPaddingRelative(startPadding, 0, endPadding, 0);
+
+        mKeyguardUnlockAnimationController.setLockscreenSmartspace(mSmartspaceView);
+    }
+
     /**
      * Apply dp changes on font/scale change
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 083f2fe..8921780 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -224,6 +224,7 @@
         @Override
         public void onLocaleListChanged() {
             refreshTime();
+            mKeyguardClockSwitchController.onLocaleListChanged();
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
index 2035781..4cd40d2 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt
@@ -15,11 +15,9 @@
  */
 package com.android.systemui.biometrics
 
-import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener
 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
 
 /**
@@ -30,7 +28,6 @@
     statusBarStateController: StatusBarStateController,
     panelExpansionStateManager: PanelExpansionStateManager,
     systemUIDialogManager: SystemUIDialogManager,
-    val broadcastSender: BroadcastSender,
     dumpManager: DumpManager
 ) : UdfpsAnimationViewController<UdfpsBpView>(
     view,
@@ -40,29 +37,4 @@
     dumpManager
 ) {
     override val tag = "UdfpsBpViewController"
-    private val bpPanelExpansionListener = PanelExpansionListener { event ->
-        // Notification shade can be expanded but not visible (fraction: 0.0), for example
-        // when a heads-up notification (HUN) is showing.
-        notificationShadeVisible = event.expanded && event.fraction > 0f
-        view.onExpansionChanged(event.fraction)
-        cancelAuth()
-    }
-
-    fun cancelAuth() {
-        if (shouldPauseAuth()) {
-            broadcastSender.closeSystemDialogs()
-        }
-    }
-
-    override fun onViewAttached() {
-        super.onViewAttached()
-
-        panelExpansionStateManager.addExpansionListener(bpPanelExpansionListener)
-    }
-
-    override fun onViewDetached() {
-        super.onViewDetached()
-
-        panelExpansionStateManager.removeExpansionListener(bpPanelExpansionListener)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 7775f50..431e88a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -24,10 +24,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.graphics.Point;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.display.DisplayManager;
@@ -54,7 +51,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.biometrics.dagger.BiometricsBackground;
-import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
@@ -128,7 +124,6 @@
     @NonNull private final UnlockedScreenOffAnimationController
             mUnlockedScreenOffAnimationController;
     @NonNull private final LatencyTracker mLatencyTracker;
-    @NonNull private final BroadcastSender mBroadcastSender;
     @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
     @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
 
@@ -209,7 +204,7 @@
                             mUnlockedScreenOffAnimationController, mHalControlsIllumination,
                             mHbmProvider, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
-                                    fromUdfpsView), mActivityLaunchAnimator, mBroadcastSender)));
+                                    fromUdfpsView), mActivityLaunchAnimator)));
         }
 
         @Override
@@ -338,20 +333,6 @@
         return velocity > 750f;
     }
 
-    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (mOverlay != null
-                    && mOverlay.getRequestReason() != REASON_AUTH_KEYGUARD
-                    && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
-                Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, mRequestReason: "
-                        + mOverlay.getRequestReason());
-                mOverlay.cancel();
-                hideUdfpsOverlay();
-            }
-        }
-    };
-
     /**
      * Forwards touches to the udfps controller / view
      */
@@ -606,7 +587,6 @@
             @NonNull LatencyTracker latencyTracker,
             @NonNull ActivityLaunchAnimator activityLaunchAnimator,
             @NonNull Optional<AlternateUdfpsTouchProvider> aternateTouchProvider,
-            @NonNull BroadcastSender broadcastSender,
             @BiometricsBackground Executor biometricsExecutor) {
         mContext = context;
         mExecution = execution;
@@ -637,7 +617,6 @@
         mLatencyTracker = latencyTracker;
         mActivityLaunchAnimator = activityLaunchAnimator;
         mAlternateTouchProvider = aternateTouchProvider.orElse(null);
-        mBroadcastSender = broadcastSender;
         mBiometricExecutor = biometricsExecutor;
 
         mOrientationListener = new BiometricDisplayListener(
@@ -655,11 +634,6 @@
         final UdfpsOverlayController mUdfpsOverlayController = new UdfpsOverlayController();
         mFingerprintManager.setUdfpsOverlayController(mUdfpsOverlayController);
 
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
-        context.registerReceiver(mBroadcastReceiver, filter,
-                Context.RECEIVER_EXPORTED_UNAUDITED);
-
         udfpsHapticsSimulator.setUdfpsController(this);
         udfpsShell.setUdfpsOverlayController(mUdfpsOverlayController);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 37db2bd..ec72057 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -41,7 +41,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -84,8 +83,7 @@
     @ShowReason val requestReason: Int,
     private val controllerCallback: IUdfpsOverlayControllerCallback,
     private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-    private val activityLaunchAnimator: ActivityLaunchAnimator,
-    private val broadcastSender: BroadcastSender
+    private val activityLaunchAnimator: ActivityLaunchAnimator
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -104,8 +102,8 @@
         fitInsetsTypes = 0
         gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-        flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS
-            or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
+        flags =
+            (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
         privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
         // Avoid announcing window title.
         accessibilityTitle = " "
@@ -227,7 +225,6 @@
                     statusBarStateController,
                     panelExpansionStateManager,
                     dialogManager,
-                    broadcastSender,
                     dumpManager
                 )
             }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index 994c630..99ca3c7 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -22,6 +22,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.os.PatternMatcher;
 import android.os.RemoteException;
@@ -31,7 +32,6 @@
 import android.util.Log;
 
 import com.android.systemui.CoreStartable;
-import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 
 import javax.inject.Inject;
@@ -66,23 +66,15 @@
         final int enabledState =
                 packageManager.getComponentEnabledSetting(mOverlayServiceComponent);
 
-
-        // TODO(b/204626521): We should not have to set the component enabled setting if the
-        // enabled config flag is properly applied based on the RRO.
-        if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) {
-            final int overlayState = mResources.getBoolean(R.bool.config_dreamOverlayServiceEnabled)
-                    ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
-                    : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
-
-            if (overlayState != enabledState) {
-                packageManager
-                        .setComponentEnabledSetting(mOverlayServiceComponent, overlayState, 0);
-            }
-        }
-
         // The overlay service is only registered when its component setting is enabled.
-        boolean register = packageManager.getComponentEnabledSetting(mOverlayServiceComponent)
-                == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+        boolean register = false;
+
+        try {
+            register = packageManager.getServiceInfo(mOverlayServiceComponent,
+                PackageManager.GET_META_DATA).enabled;
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "could not find dream overlay service");
+        }
 
         if (mCurrentRegisteredState == register) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index ce50ddf..fcafead 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -283,6 +283,7 @@
                 .inflate(R.layout.qs_paged_page, this, false);
         page.setMinRows(mMinRows);
         page.setMaxColumns(mMaxColumns);
+        page.setSelected(false);
         return page;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
index 7cfb157..fb71210 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationUtils.java
@@ -25,6 +25,7 @@
 import com.android.internal.util.ContrastColorUtil;
 import com.android.systemui.R;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.util.Compile;
 
 /**
@@ -89,6 +90,11 @@
         }
     }
 
+    /** Get the notification key, reformatted for logging, for the (optional) row */
+    public static String logKey(ExpandableNotificationRow row) {
+        return row == null ? "null" : logKey(row.getEntry());
+    }
+
     /** Removes newlines from the notification key to prettify apps that have these in the tag */
     public static String logKey(String key) {
         if (key == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
index 4daed77..6a3799b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifInflaterImpl.java
@@ -79,6 +79,11 @@
         entry.abortTask();
     }
 
+    @Override
+    public void releaseViews(@NonNull NotificationEntry entry) {
+        requireBinder().releaseViews(entry);
+    }
+
     private NotificationContentInflater.InflationCallback wrapInflationCallback(
             InflationCallback callback) {
         return new NotificationContentInflater.InflationCallback() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 210fe8f..8f37baf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -376,6 +376,7 @@
 
     private void freeNotifViews(NotificationEntry entry) {
         mViewBarn.removeViewForEntry(entry);
+        mNotifInflater.releaseViews(entry);
         // TODO: clear the entry's row here, or even better, stop setting the row on the entry!
         mInflationStates.put(entry, STATE_UNINFLATED);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index d98e7f7..567ec85 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -47,6 +47,11 @@
     fun abortInflation(entry: NotificationEntry)
 
     /**
+     * Called to let the system remove the content views from the notification row.
+     */
+    fun releaseViews(entry: NotificationEntry)
+
+    /**
      * Callback once all the views are inflated and bound for a given NotificationEntry.
      */
     interface InflationCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
index 3a4701c..46b467e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
@@ -50,4 +50,9 @@
             NotificationUiAdjustment oldAdjustment,
             NotificationUiAdjustment newAdjustment,
             NotificationRowContentBinder.InflationCallback callback);
+
+    /**
+     * Called when a notification is no longer likely to be displayed and can have its views freed.
+     */
+    void releaseViews(NotificationEntry entry);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index b84a797..528f720 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
 import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
 
 import static java.util.Objects.requireNonNull;
@@ -161,6 +163,18 @@
         }
     }
 
+    @Override
+    public void releaseViews(NotificationEntry entry) {
+        if (!entry.rowExists()) {
+            return;
+        }
+        final RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
+        params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED);
+        params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED);
+        params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
+        mRowContentBindStage.requestRebind(entry, null);
+    }
+
     /**
      * Bind row to various controllers and managers. This is only called when the row is first
      * created.
@@ -249,6 +263,8 @@
         }
 
         RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
+        params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED);
+        params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED);
         params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
         params.setUseLowPriority(isLowPriority);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 9acd60e..ea28452 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -16,13 +16,17 @@
 
 package com.android.systemui.statusbar.notification.stack;
 
+import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.util.MathUtils;
 
+import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -33,13 +37,15 @@
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 
+import java.io.PrintWriter;
+
 import javax.inject.Inject;
 
 /**
  * A global state to track all input states for the algorithm.
  */
 @SysUISingleton
-public class AmbientState {
+public class AmbientState implements Dumpable {
 
     private static final float MAX_PULSE_HEIGHT = 100000f;
     private static final boolean NOTIFICATIONS_HAVE_SHADOWS = false;
@@ -224,7 +230,8 @@
 
     @Inject
     public AmbientState(
-            Context context,
+            @NonNull Context context,
+            @NonNull DumpManager dumpManager,
             @NonNull SectionProvider sectionProvider,
             @NonNull BypassController bypassController,
             @Nullable StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
@@ -232,6 +239,7 @@
         mBypassController = bypassController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         reload(context);
+        dumpManager.registerDumpable(this);
     }
 
     /**
@@ -695,4 +703,49 @@
         return mStatusBarKeyguardViewManager != null
                 && mStatusBarKeyguardViewManager.isBouncerInTransit();
     }
+
+    @Override
+    public void dump(PrintWriter pw, String[] args) {
+        pw.println("mTopPadding=" + mTopPadding);
+        pw.println("mStackTopMargin=" + mStackTopMargin);
+        pw.println("mStackTranslation=" + mStackTranslation);
+        pw.println("mLayoutMinHeight=" + mLayoutMinHeight);
+        pw.println("mLayoutMaxHeight=" + mLayoutMaxHeight);
+        pw.println("mLayoutHeight=" + mLayoutHeight);
+        pw.println("mContentHeight=" + mContentHeight);
+        pw.println("mHideSensitive=" + mHideSensitive);
+        pw.println("mShadeExpanded=" + mShadeExpanded);
+        pw.println("mClearAllInProgress=" + mClearAllInProgress);
+        pw.println("mDimmed=" + mDimmed);
+        pw.println("mStatusBarState=" + mStatusBarState);
+        pw.println("mExpansionChanging=" + mExpansionChanging);
+        pw.println("mPanelFullWidth=" + mPanelFullWidth);
+        pw.println("mPulsing=" + mPulsing);
+        pw.println("mPulseHeight=" + mPulseHeight);
+        pw.println("mTrackedHeadsUpRow.key=" + logKey(mTrackedHeadsUpRow));
+        pw.println("mMaxHeadsUpTranslation=" + mMaxHeadsUpTranslation);
+        pw.println("mUnlockHintRunning=" + mUnlockHintRunning);
+        pw.println("mDozeAmount=" + mDozeAmount);
+        pw.println("mDozing=" + mDozing);
+        pw.println("mFractionToShade=" + mFractionToShade);
+        pw.println("mHideAmount=" + mHideAmount);
+        pw.println("mAppearFraction=" + mAppearFraction);
+        pw.println("mAppearing=" + mAppearing);
+        pw.println("mExpansionFraction=" + mExpansionFraction);
+        pw.println("mExpandingVelocity=" + mExpandingVelocity);
+        pw.println("mOverScrollTopAmount=" + mOverScrollTopAmount);
+        pw.println("mOverScrollBottomAmount=" + mOverScrollBottomAmount);
+        pw.println("mOverExpansion=" + mOverExpansion);
+        pw.println("mStackHeight=" + mStackHeight);
+        pw.println("mStackEndHeight=" + mStackEndHeight);
+        pw.println("mStackY=" + mStackY);
+        pw.println("mScrollY=" + mScrollY);
+        pw.println("mCurrentScrollVelocity=" + mCurrentScrollVelocity);
+        pw.println("mIsSwipingUp=" + mIsSwipingUp);
+        pw.println("mPanelTracking=" + mPanelTracking);
+        pw.println("mIsFlinging=" + mIsFlinging);
+        pw.println("mNeedFlingAfterLockscreenSwipeUp=" + mNeedFlingAfterLockscreenSwipeUp);
+        pw.println("mZDistanceBetweenElements=" + mZDistanceBetweenElements);
+        pw.println("mBaseZHeight=" + mBaseZHeight);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index b8abb25..d54a554 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -19,6 +19,8 @@
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
+import static com.android.systemui.util.DumpUtilsKt.println;
+import static com.android.systemui.util.DumpUtilsKt.visibilityString;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
@@ -5027,24 +5029,31 @@
     @ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
     public void dump(PrintWriter pwOriginal, String[] args) {
         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
-        StringBuilder sb = new StringBuilder("[")
-                .append(this.getClass().getSimpleName()).append(":")
-                .append(" pulsing=").append(mPulsing ? "T" : "f")
-                .append(" expanded=").append(mIsExpanded ? "T" : "f")
-                .append(" headsUpPinned=").append(mInHeadsUpPinnedMode ? "T" : "f")
-                .append(" qsClipping=").append(mShouldUseRoundedRectClipping ? "T" : "f")
-                .append(" qsClipDismiss=").append(mDismissUsingRowTranslationX ? "T" : "f")
-                .append(" visibility=").append(DumpUtilsKt.visibilityString(getVisibility()))
-                .append(" alpha=").append(getAlpha())
-                .append(" scrollY=").append(mAmbientState.getScrollY())
-                .append(" maxTopPadding=").append(mMaxTopPadding)
-                .append(" showShelfOnly=").append(mShouldShowShelfOnly ? "T" : "f")
-                .append(" qsExpandFraction=").append(mQsExpansionFraction)
-                .append(" isCurrentUserSetup=").append(mIsCurrentUserSetup)
-                .append(" hideAmount=").append(mAmbientState.getHideAmount())
-                .append(" ambientStateSwipingUp=").append(mAmbientState.isSwipingUp())
-                .append("]");
-        pw.println(sb.toString());
+        pw.println("Internal state:");
+        DumpUtilsKt.withIncreasedIndent(pw, () -> {
+            println(pw, "pulsing", mPulsing);
+            println(pw, "expanded", mIsExpanded);
+            println(pw, "headsUpPinned", mInHeadsUpPinnedMode);
+            println(pw, "qsClipping", mShouldUseRoundedRectClipping);
+            println(pw, "qsClipDismiss", mDismissUsingRowTranslationX);
+            println(pw, "visibility", visibilityString(getVisibility()));
+            println(pw, "alpha", getAlpha());
+            println(pw, "scrollY", mAmbientState.getScrollY());
+            println(pw, "maxTopPadding", mMaxTopPadding);
+            println(pw, "showShelfOnly", mShouldShowShelfOnly);
+            println(pw, "qsExpandFraction", mQsExpansionFraction);
+            println(pw, "isCurrentUserSetup", mIsCurrentUserSetup);
+            println(pw, "hideAmount", mAmbientState.getHideAmount());
+            println(pw, "ambientStateSwipingUp", mAmbientState.isSwipingUp());
+            println(pw, "maxDisplayedNotifications", mMaxDisplayedNotifications);
+            println(pw, "intrinsicContentHeight", mIntrinsicContentHeight);
+            println(pw, "contentHeight", mContentHeight);
+            println(pw, "intrinsicPadding", mIntrinsicPadding);
+            println(pw, "topPadding", mTopPadding);
+            println(pw, "bottomPadding", mBottomPadding);
+        });
+        pw.println();
+        pw.println("Contents:");
         DumpUtilsKt.withIncreasedIndent(pw, () -> {
             int childCount = getChildCount();
             pw.println("Number of children: " + childCount);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 2493ccb..1e69ee6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -68,6 +68,7 @@
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -156,6 +157,7 @@
     private final ConfigurationController mConfigurationController;
     private final ZenModeController mZenModeController;
     private final MetricsLogger mMetricsLogger;
+    private final DumpManager mDumpManager;
     private final FalsingCollector mFalsingCollector;
     private final FalsingManager mFalsingManager;
     private final Resources mResources;
@@ -635,6 +637,7 @@
             SysuiColorExtractor colorExtractor,
             NotificationLockscreenUserManager lockscreenUserManager,
             MetricsLogger metricsLogger,
+            DumpManager dumpManager,
             FalsingCollector falsingCollector,
             FalsingManager falsingManager,
             @Main Resources resources,
@@ -677,6 +680,7 @@
         mZenModeController = zenModeController;
         mLockscreenUserManager = lockscreenUserManager;
         mMetricsLogger = metricsLogger;
+        mDumpManager = dumpManager;
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mShadeTransitionController = shadeTransitionController;
         mFalsingCollector = falsingCollector;
@@ -728,6 +732,7 @@
             }
         });
         mView.setShadeController(mShadeController);
+        mDumpManager.registerDumpable(mView);
 
         mKeyguardBypassController.registerOnBypassStateChangedListener(
                 isEnabled -> mNotificationRoundnessManager.setShouldRoundPulsingViews(!isEnabled));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b3e0073..8c61764 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2354,15 +2354,7 @@
             pw.print  ("      ");
             mNotificationPanelViewController.dump(pw, args);
         }
-        pw.println("  mStackScroller: ");
-        if (mStackScroller != null) {
-            // Double indent until we rewrite the rest of this dump()
-            pw.increaseIndent();
-            pw.increaseIndent();
-            mStackScroller.dump(pw, args);
-            pw.decreaseIndent();
-            pw.decreaseIndent();
-        }
+        pw.println("  mStackScroller: " + mStackScroller + " (dump moved)");
         pw.println("  Theme:");
         String nightMode = mUiModeManager == null ? "null" : mUiModeManager.getNightMode() + "";
         pw.println("    dark theme: " + nightMode +
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index 779c6b4..def574c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -23,12 +23,16 @@
 import android.animation.ValueAnimator;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.database.ContentObserver;
 import android.hardware.biometrics.BiometricSourceType;
+import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.util.MathUtils;
 import android.view.View;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.keyguard.CarrierTextController;
 import com.android.keyguard.KeyguardUpdateMonitor;
@@ -36,6 +40,7 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.battery.BatteryMeterViewController;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
@@ -45,6 +50,7 @@
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.phone.fragment.StatusBarIconBlocklistKt;
 import com.android.systemui.statusbar.phone.fragment.StatusBarSystemEventAnimator;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserInfoTracker;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherController;
@@ -54,10 +60,12 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.util.ViewController;
+import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -98,6 +106,9 @@
     private final StatusBarUserSwitcherFeatureController mFeatureController;
     private final StatusBarUserSwitcherController mUserSwitcherController;
     private final StatusBarUserInfoTracker mStatusBarUserInfoTracker;
+    private final SecureSettings mSecureSettings;
+    private final Executor mMainExecutor;
+    private final Object mLock = new Object();
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -206,7 +217,7 @@
                 }
             };
 
-    private final List<String> mBlockedIcons;
+    private final List<String> mBlockedIcons = new ArrayList<>();
     private final int mNotificationsHeaderCollideDistance;
 
     private boolean mBatteryListening;
@@ -255,7 +266,9 @@
             UserManager userManager,
             StatusBarUserSwitcherFeatureController featureController,
             StatusBarUserSwitcherController userSwitcherController,
-            StatusBarUserInfoTracker statusBarUserInfoTracker
+            StatusBarUserInfoTracker statusBarUserInfoTracker,
+            SecureSettings secureSettings,
+            @Main Executor mainExecutor
     ) {
         super(view);
         mCarrierTextController = carrierTextController;
@@ -277,6 +290,8 @@
         mFeatureController = featureController;
         mUserSwitcherController = userSwitcherController;
         mStatusBarUserInfoTracker = statusBarUserInfoTracker;
+        mSecureSettings = secureSettings;
+        mMainExecutor = mainExecutor;
 
         mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled();
         mKeyguardStateController.addCallback(
@@ -292,8 +307,7 @@
         );
 
         Resources r = getResources();
-        mBlockedIcons = Arrays.asList(r.getStringArray(
-                R.array.config_keyguard_statusbar_icon_blocklist));
+        updateBlockedIcons();
         mNotificationsHeaderCollideDistance = r.getDimensionPixelSize(
                 R.dimen.header_notifications_collide_distance);
 
@@ -321,11 +335,16 @@
         if (mTintedIconManager == null) {
             mTintedIconManager =
                     mTintedIconManagerFactory.create(mView.findViewById(R.id.statusIcons));
-            mTintedIconManager.setBlockList(mBlockedIcons);
+            mTintedIconManager.setBlockList(getBlockedIcons());
             mStatusBarIconController.addIconGroup(mTintedIconManager);
         }
         mView.setOnApplyWindowInsetsListener(
                 (view, windowInsets) -> mView.updateWindowInsets(windowInsets, mInsetsProvider));
+        mSecureSettings.registerContentObserverForUser(
+                Settings.Secure.getUriFor(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON),
+                false,
+                mVolumeSettingObserver,
+                UserHandle.USER_ALL);
         updateUserSwitcher();
         onThemeChanged();
     }
@@ -337,6 +356,7 @@
         mUserInfoController.removeCallback(mOnUserInfoChangedListener);
         mStatusBarStateController.removeCallback(mStatusBarStateListener);
         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
+        mSecureSettings.unregisterContentObserver(mVolumeSettingObserver);
         if (mTintedIconManager != null) {
             mStatusBarIconController.removeIconGroup(mTintedIconManager);
         }
@@ -486,8 +506,32 @@
                 R.bool.qs_show_user_switcher_for_single_user)));
     }
 
+    @VisibleForTesting
+    void updateBlockedIcons() {
+        List<String> newBlockList = StatusBarIconBlocklistKt
+                .getStatusBarIconBlocklist(getResources(), mSecureSettings);
+
+        synchronized (mLock) {
+            mBlockedIcons.clear();
+            mBlockedIcons.addAll(newBlockList);
+        }
+
+        mMainExecutor.execute(() -> {
+            if (mTintedIconManager != null) {
+                mTintedIconManager.setBlockList(getBlockedIcons());
+            }
+        });
+    }
+
+    @VisibleForTesting
+    List<String> getBlockedIcons() {
+        synchronized (mLock) {
+            return new ArrayList<>(mBlockedIcons);
+        }
+    }
+
     /**
-     * Update {@link KeyguardStatusBarView}'s visibility based on whether keyguard is showing and
+      Update {@link KeyguardStatusBarView}'s visibility based on whether keyguard is showing and
      * whether heads up is visible.
      */
     public void updateForHeadsUp() {
@@ -533,4 +577,11 @@
         mExplicitAlpha = alpha;
         updateViewState();
     }
+
+    private final ContentObserver mVolumeSettingObserver = new ContentObserver(null) {
+        @Override
+        public void onChange(boolean selfChange) {
+            updateBlockedIcons();
+        }
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt
index 178c17d..84c8700 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LargeScreenShadeHeaderController.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone
 
 import android.app.StatusBarManager
+import android.content.res.Configuration
 import android.view.View
 import android.widget.TextView
 import androidx.constraintlayout.motion.widget.MotionLayout
@@ -204,6 +205,12 @@
 
     private fun bindConfigurationListener() {
         val listener = object : ConfigurationController.ConfigurationListener {
+            override fun onConfigChanged(newConfig: Configuration?) {
+                val left = header.resources.getDimensionPixelSize(
+                    R.dimen.large_screen_shade_header_left_padding)
+                header.setPadding(
+                    left, header.paddingTop, header.paddingRight, header.paddingBottom)
+            }
             override fun onDensityOrFontScaleChanged() {
                 val qsStatusStyle = R.style.TextAppearance_QS_Status
                 FontSizeUtils.updateFontSizeFromStyle(clock, qsStatusStyle)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarIconBlocklist.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarIconBlocklist.kt
new file mode 100644
index 0000000..b845bad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/StatusBarIconBlocklist.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.phone.fragment
+
+import android.content.res.Resources
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.internal.R
+import com.android.systemui.util.settings.SecureSettings
+
+/**
+ * Centralize the logic for the status bar / keyguard status bar icon blocklist. The default is
+ * loaded from the config, and we currently support a system setting for the vibrate icon. It's
+ * pretty likely that we would end up supporting more user-configurable settings in the future, so
+ * breaking this out into its own file for now.
+ *
+ * Note for the future: it might be reasonable to turn this into its own class that can listen to
+ * the system setting and execute a callback when it changes instead of having multiple content
+ * observers.
+ */
+fun getStatusBarIconBlocklist(
+    res: Resources,
+    settings: SecureSettings
+): List<String> {
+    // Load the default blocklist from res
+    val blocklist = res.getStringArray(
+            com.android.systemui.R.array.config_collapsed_statusbar_icon_blocklist).toList()
+
+    val vibrateIconSlot: String = res.getString(R.string.status_bar_volume)
+    val showVibrateIcon = settings.getIntForUser(
+            Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON,
+            0,
+            UserHandle.USER_CURRENT) == 0
+
+    // Filter out vibrate icon from the blocklist if the setting is on
+    return blocklist.filter { icon ->
+        !icon.equals(vibrateIconSlot) || showVibrateIcon
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/DelayableMarqueeTextView.kt b/packages/SystemUI/src/com/android/systemui/util/DelayableMarqueeTextView.kt
new file mode 100644
index 0000000..8b90547
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/DelayableMarqueeTextView.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.R
+
+class DelayableMarqueeTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : SafeMarqueeTextView(context, attrs, defStyleAttr, defStyleRes) {
+
+    var marqueeDelay: Long = DEFAULT_MARQUEE_DELAY
+    private var wantsMarquee = false
+    private var marqueeBlocked = true
+
+    private val enableMarquee = Runnable {
+        if (wantsMarquee) {
+            marqueeBlocked = false
+            startMarquee()
+        }
+    }
+
+    init {
+        val typedArray = context.theme.obtainStyledAttributes(
+                attrs,
+                R.styleable.DelayableMarqueeTextView,
+                defStyleAttr,
+                defStyleRes
+        )
+        marqueeDelay = typedArray.getInteger(
+                R.styleable.DelayableMarqueeTextView_marqueeDelay,
+                DEFAULT_MARQUEE_DELAY.toInt()
+        ).toLong()
+        typedArray.recycle()
+    }
+
+    override fun startMarquee() {
+        if (!isSelected) {
+            return
+        }
+        wantsMarquee = true
+        if (marqueeBlocked) {
+            if (handler?.hasCallbacks(enableMarquee) == false) {
+                postDelayed(enableMarquee, marqueeDelay)
+            }
+            return
+        }
+        super.startMarquee()
+    }
+
+    override fun stopMarquee() {
+        handler?.removeCallbacks(enableMarquee)
+        wantsMarquee = false
+        marqueeBlocked = true
+        super.stopMarquee()
+    }
+
+    companion object {
+        const val DEFAULT_MARQUEE_DELAY = 2000L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
index f952476..018ef96 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/DumpUtils.kt
@@ -55,6 +55,10 @@
     }
 }
 
+/** Print a line which is '$label=$value' */
+fun IndentingPrintWriter.println(label: String, value: Any) =
+    append(label).append('=').println(value)
+
 /** Return a readable string for the visibility */
 fun visibilityString(@View.Visibility visibility: Int): String = when (visibility) {
     View.GONE -> "gone"
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index fe9e75c..5db2cf4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -229,13 +229,22 @@
     @Test
     public void testSmartspaceEnabledRemovesKeyguardStatusArea() {
         when(mSmartspaceController.isEnabled()).thenReturn(true);
-        when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView);
         mController.init();
 
         assertEquals(View.GONE, mSliceView.getVisibility());
     }
 
     @Test
+    public void onLocaleListChangedRebuildsSmartspaceView() {
+        when(mSmartspaceController.isEnabled()).thenReturn(true);
+        mController.init();
+
+        mController.onLocaleListChanged();
+        // Should be called once on initial setup, then once again for locale change
+        verify(mSmartspaceController, times(2)).buildAndConnectView(mView);
+    }
+
+    @Test
     public void testSmartspaceDisabledShowsKeyguardStatusArea() {
         when(mSmartspaceController.isEnabled()).thenReturn(false);
         mController.init();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index 650a5d0..70025230 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
@@ -117,4 +118,16 @@
 
         verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
     }
+
+    @Test
+    public void onLocaleListChangedNotifiesClockSwitchController() {
+        ArgumentCaptor<ConfigurationListener> configurationListenerArgumentCaptor =
+                ArgumentCaptor.forClass(ConfigurationListener.class);
+
+        mController.onViewAttached();
+        verify(mConfigurationController).addCallback(configurationListenerArgumentCaptor.capture());
+
+        configurationListenerArgumentCaptor.getValue().onLocaleListChanged();
+        verify(mKeyguardClockSwitchController).onLocaleListChanged();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
deleted file mode 100644
index a52c4a3..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics
-
-import android.app.Instrumentation
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper
-import android.testing.ViewUtils
-import androidx.test.filters.SmallTest
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.R
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.broadcast.BroadcastSender
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.statusbar.StatusBarStateControllerImpl
-import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
-import com.android.systemui.util.mockito.any
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.spy
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@TestableLooper.RunWithLooper
-class UdfpsBpViewControllerTest : SysuiTestCase() {
-
-    @JvmField @Rule var rule = MockitoJUnit.rule()
-
-    @Mock lateinit var dumpManager: DumpManager
-    @Mock lateinit var systemUIDialogManager: SystemUIDialogManager
-    @Mock lateinit var broadcastSender: BroadcastSender
-    @Mock lateinit var interactionJankMonitor: InteractionJankMonitor
-    @Mock lateinit var panelExpansionStateManager: PanelExpansionStateManager
-
-    private lateinit var instrumentation: Instrumentation
-    private lateinit var uiEventLogger: UiEventLoggerFake
-    private lateinit var udfpsBpView: UdfpsBpView
-    private lateinit var statusBarStateController: StatusBarStateControllerImpl
-    private lateinit var udfpsBpViewController: UdfpsBpViewController
-
-    @Before
-    fun setup() {
-        instrumentation = getInstrumentation()
-        instrumentation.runOnMainSync { createUdfpsView() }
-        instrumentation.waitForIdleSync()
-
-        uiEventLogger = UiEventLoggerFake()
-        statusBarStateController =
-            StatusBarStateControllerImpl(uiEventLogger, dumpManager, interactionJankMonitor)
-        udfpsBpViewController = UdfpsBpViewController(
-            udfpsBpView,
-            statusBarStateController,
-            panelExpansionStateManager,
-            systemUIDialogManager,
-            broadcastSender,
-            dumpManager)
-        udfpsBpViewController.init()
-    }
-
-    @After
-    fun tearDown() {
-        if (udfpsBpViewController.isAttachedToWindow) {
-            instrumentation.runOnMainSync { ViewUtils.detachView(udfpsBpView) }
-            instrumentation.waitForIdleSync()
-        }
-    }
-
-    private fun createUdfpsView() {
-        context.setTheme(R.style.Theme_AppCompat)
-        context.orCreateTestableResources.addOverride(
-            com.android.internal.R.integer.config_udfps_illumination_transition_ms, 0)
-        udfpsBpView = UdfpsBpView(context, null)
-    }
-
-    @Test
-    fun addExpansionListener() {
-        instrumentation.runOnMainSync { ViewUtils.attachView(udfpsBpView) }
-        instrumentation.waitForIdleSync()
-
-        // Both UdfpsBpViewController & UdfpsAnimationViewController add listener
-        verify(panelExpansionStateManager, times(2)).addExpansionListener(any())
-    }
-
-    @Test
-    fun removeExpansionListener() {
-        instrumentation.runOnMainSync { ViewUtils.attachView(udfpsBpView) }
-        instrumentation.waitForIdleSync()
-        instrumentation.runOnMainSync { ViewUtils.detachView(udfpsBpView) }
-        instrumentation.waitForIdleSync()
-
-        // Both UdfpsBpViewController & UdfpsAnimationViewController remove listener
-        verify(panelExpansionStateManager, times(2)).removeExpansionListener(any())
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 431739b..cb8358d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -40,7 +40,6 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.broadcast.BroadcastSender
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -103,7 +102,6 @@
     @Mock private lateinit var udfpsView: UdfpsView
     @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
-    @Mock private lateinit var broadcastSender: BroadcastSender
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -133,8 +131,7 @@
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, HAL_CONTROLS_ILLUMINATION, hbmProvider,
-            REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator,
-            broadcastSender)
+            REQUEST_ID, reason, controllerCallback, onTouch, activityLaunchAnimator)
         block()
     }
 
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 554e27d..638e6f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -66,7 +66,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.plugins.FalsingManager;
@@ -188,8 +187,6 @@
     private ActivityLaunchAnimator mActivityLaunchAnimator;
     @Mock
     private AlternateUdfpsTouchProvider mAlternateTouchProvider;
-    @Mock
-    private BroadcastSender mBroadcastSender;
 
     // Capture listeners so that they can be used to send events
     @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor;
@@ -270,7 +267,6 @@
                 mLatencyTracker,
                 mActivityLaunchAnimator,
                 Optional.of(mAlternateTouchProvider),
-                mBroadcastSender,
                 mBiometricsExecutor);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index d327be4..72d8ff3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -449,6 +449,7 @@
             mInflateCallbacks.put(entry, callback);
         }
 
+
         @Override
         public void rebindViews(@NonNull NotificationEntry entry, @NonNull Params params,
                 @NonNull InflationCallback callback) {
@@ -465,6 +466,10 @@
         public void invokeInflateCallbackForEntry(NotificationEntry entry) {
             getInflateCallback(entry).onInflationFinished(entry, entry.getRowController());
         }
+
+        @Override
+        public void releaseViews(@NonNull NotificationEntry entry) {
+        }
     }
 
     private void fireAddEvents(List<? extends ListEntry> entries) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 94a93ad..4efd5c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -47,6 +47,7 @@
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener;
@@ -113,6 +114,7 @@
     @Mock private SysuiColorExtractor mColorExtractor;
     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
     @Mock private MetricsLogger mMetricsLogger;
+    @Mock private DumpManager mDumpManager;
     @Mock private Resources mResources;
     @Mock(answer = Answers.RETURNS_SELF)
     private NotificationSwipeHelper.Builder mNotificationSwipeHelperBuilder;
@@ -167,6 +169,7 @@
                 mColorExtractor,
                 mNotificationLockscreenUserManager,
                 mMetricsLogger,
+                mDumpManager,
                 new FalsingCollectorFake(),
                 new FalsingManagerFake(),
                 mResources,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 9961aae..f5fe6f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -57,6 +57,7 @@
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationShelfController;
@@ -101,6 +102,7 @@
     @Mock private SysuiStatusBarStateController mBarState;
     @Mock private NotificationGroupManagerLegacy mGroupMembershipManger;
     @Mock private NotificationGroupManagerLegacy mGroupExpansionManager;
+    @Mock private DumpManager mDumpManager;
     @Mock private ExpandHelper mExpandHelper;
     @Mock private EmptyShadeView mEmptyShadeView;
     @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
@@ -120,7 +122,11 @@
         allowTestableLooperAsMainThread();
 
         // Interact with real instance of AmbientState.
-        mAmbientState = new AmbientState(mContext, mNotificationSectionsManager, mBypassController,
+        mAmbientState = new AmbientState(
+                mContext,
+                mDumpManager,
+                mNotificationSectionsManager,
+                mBypassController,
                 mStatusBarKeyguardViewManager);
 
         // Inject dependencies before initializing the layout
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 668f752..275dbfd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -5,6 +5,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.EmptyShadeView
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.ExpandableView
@@ -26,10 +27,12 @@
     private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
     private val expandableViewState = ExpandableViewState()
     private val notificationRow = mock(ExpandableNotificationRow::class.java)
+    private val dumpManager = mock(DumpManager::class.java)
     private val mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager::class.java)
 
     private val ambientState = AmbientState(
         context,
+        dumpManager,
         SectionProvider { _, _ -> false },
         BypassController { false },
         mStatusBarKeyguardViewManager
@@ -126,7 +129,7 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = viewStart
 
-        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart);
+        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
         assertFalse(expandableViewState.hidden)
     }
 
@@ -142,7 +145,7 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = viewStart
 
-        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart);
+        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
         assertTrue(expandableViewState.hidden)
     }
 
@@ -158,7 +161,7 @@
         val expandableViewState = ExpandableViewState()
         expandableViewState.yTranslation = viewStart
 
-        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart);
+        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
         assertFalse(expandableViewState.hidden)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 39d5a16..4e1a708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -22,6 +22,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.clearInvocations;
@@ -29,7 +31,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.LayoutInflater;
@@ -55,6 +59,9 @@
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.FakeSystemClock;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -108,10 +115,12 @@
     private StatusBarUserSwitcherController mStatusBarUserSwitcherController;
     @Mock
     private StatusBarUserInfoTracker mStatusBarUserInfoTracker;
+    @Mock private SecureSettings mSecureSettings;
 
     private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider;
     private KeyguardStatusBarView mKeyguardStatusBarView;
     private KeyguardStatusBarViewController mController;
+    private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
 
     @Before
     public void setup() throws Exception {
@@ -150,7 +159,9 @@
                 mUserManager,
                 mStatusBarUserSwitcherFeatureController,
                 mStatusBarUserSwitcherController,
-                mStatusBarUserInfoTracker
+                mStatusBarUserInfoTracker,
+                mSecureSettings,
+                mFakeExecutor
         );
     }
 
@@ -420,6 +431,39 @@
         assertThat(mKeyguardStatusBarView.isKeyguardUserAvatarEnabled()).isTrue();
     }
 
+    @Test
+    public void testBlockedIcons_obeysSettingForVibrateIcon_settingOff() {
+        String str = mContext.getString(com.android.internal.R.string.status_bar_volume);
+
+        // GIVEN the setting is off
+        when(mSecureSettings.getInt(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, 0))
+                .thenReturn(0);
+
+        // WHEN CollapsedStatusBarFragment builds the blocklist
+        mController.updateBlockedIcons();
+
+        // THEN status_bar_volume SHOULD be present in the list
+        boolean contains = mController.getBlockedIcons().contains(str);
+        assertTrue(contains);
+    }
+
+    @Test
+    public void testBlockedIcons_obeysSettingForVibrateIcon_settingOn() {
+        String str = mContext.getString(com.android.internal.R.string.status_bar_volume);
+
+        // GIVEN the setting is ON
+        when(mSecureSettings.getIntForUser(Settings.Secure.STATUS_BAR_SHOW_VIBRATE_ICON, 0,
+                UserHandle.USER_CURRENT))
+                .thenReturn(1);
+
+        // WHEN CollapsedStatusBarFragment builds the blocklist
+        mController.updateBlockedIcons();
+
+        // THEN status_bar_volume SHOULD NOT be present in the list
+        boolean contains = mController.getBlockedIcons().contains(str);
+        assertFalse(contains);
+    }
+
     private void updateStateToNotKeyguard() {
         updateStatusBarState(SHADE);
     }
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index e62f35d..e90a5db 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2548,28 +2548,31 @@
     public void sendPendingBroadcasts() {
         String[] packages;
         ArrayList<String>[] components;
-        int size = 0;
+        int numBroadcasts = 0, numUsers;
         int[] uids;
 
         synchronized (mPm.mLock) {
             final SparseArray<ArrayMap<String, ArrayList<String>>> userIdToPackagesToComponents =
                     mPm.mPendingBroadcasts.copiedMap();
-            size = userIdToPackagesToComponents.size();
-            if (size <= 0) {
+            numUsers = userIdToPackagesToComponents.size();
+            for (int n = 0; n < numUsers; n++) {
+                numBroadcasts += userIdToPackagesToComponents.valueAt(n).size();
+            }
+            if (numBroadcasts == 0) {
                 // Nothing to be done. Just return
                 return;
             }
-            packages = new String[size];
-            components = new ArrayList[size];
-            uids = new int[size];
+            packages = new String[numBroadcasts];
+            components = new ArrayList[numBroadcasts];
+            uids = new int[numBroadcasts];
             int i = 0;  // filling out the above arrays
 
-            for (int n = 0; n < size; n++) {
+            for (int n = 0; n < numUsers; n++) {
                 final int packageUserId = userIdToPackagesToComponents.keyAt(n);
                 final ArrayMap<String, ArrayList<String>> componentsToBroadcast =
                         userIdToPackagesToComponents.valueAt(n);
                 final int numComponents = CollectionUtils.size(componentsToBroadcast);
-                for (int index = 0; i < size && index < numComponents; index++) {
+                for (int index = 0; index < numComponents; index++) {
                     packages[i] = componentsToBroadcast.keyAt(index);
                     components[i] = componentsToBroadcast.valueAt(index);
                     final PackageSetting ps = mPm.mSettings.getPackageLPr(packages[i]);
@@ -2579,12 +2582,12 @@
                     i++;
                 }
             }
-            size = i;
+            numBroadcasts = i;
             mPm.mPendingBroadcasts.clear();
         }
         final Computer snapshot = mPm.snapshotComputer();
         // Send broadcasts
-        for (int i = 0; i < size; i++) {
+        for (int i = 0; i < numBroadcasts; i++) {
             mPm.sendPackageChangedBroadcast(snapshot, packages[i], true /* dontKillApp */,
                     components[i], uids[i], null /* reason */);
         }
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 47aa587..f53b51d 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -43,6 +43,7 @@
 import static com.android.server.wm.ActivityTaskManagerService.TAG_SWITCH;
 import static com.android.server.wm.ActivityTaskManagerService.enforceNotIsolatedCaller;
 
+import android.Manifest;
 import android.annotation.ColorInt;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -462,8 +463,8 @@
                     // Explicitly dismissing the activity so reset its relaunch flag.
                     r.mRelaunchReason = RELAUNCH_REASON_NONE;
                 } else {
-                    r.finishIfPossible(resultCode, resultData, resultGrants,
-                            "app-request", true /* oomAdj */);
+                    r.finishIfPossible(resultCode, resultData, resultGrants, "app-request",
+                            true /* oomAdj */);
                     res = r.finishing;
                     if (!res) {
                         Slog.i(TAG, "Failed to finish by app-request");
@@ -525,6 +526,23 @@
     }
 
     @Override
+    public void setForceSendResultForMediaProjection(IBinder token) {
+        // Require that this is invoked only during MediaProjection setup.
+        mService.mAmInternal.enforceCallingPermission(
+                Manifest.permission.MANAGE_MEDIA_PROJECTION,
+                "setForceSendResultForMediaProjection");
+
+        final ActivityRecord r;
+        synchronized (mGlobalLock) {
+            r = ActivityRecord.isInRootTaskLocked(token);
+            if (r == null || !r.isInHistory()) {
+                return;
+            }
+            r.setForceSendResultForMediaProjection();
+        }
+    }
+
+    @Override
     public boolean isTopOfTask(IBinder token) {
         synchronized (mGlobalLock) {
             final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 0672559..f1f091a6 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -595,6 +595,8 @@
     // pre-NYC apps that don't have a sense of being resized.
     int mRelaunchReason = RELAUNCH_REASON_NONE;
 
+    private boolean mForceSendResultForMediaProjection = false;
+
     TaskDescription taskDescription; // the recents information for this activity
 
     // The locusId associated with this activity, if set.
@@ -925,7 +927,7 @@
     // SystemUi sets the pinned mode on activity after transition is done.
     boolean mWaitForEnteringPinnedMode;
 
-    private final ActivityRecordInputSink mActivityRecordInputSink;
+    final ActivityRecordInputSink mActivityRecordInputSink;
 
     // Activities with this uid are allowed to not create an input sink while being in the same
     // task and directly above this ActivityRecord. This field is updated whenever a new activity
@@ -3257,7 +3259,12 @@
                 mAtmService.mUgmInternal.grantUriPermissionUncheckedFromIntent(resultGrants,
                         resultTo.getUriPermissionsLocked());
             }
-            resultTo.addResultLocked(this, resultWho, requestCode, resultCode, resultData);
+            if (mForceSendResultForMediaProjection) {
+                resultTo.sendResult(this.getUid(), resultWho, requestCode, resultCode,
+                        resultData, resultGrants, true /* forceSendForMediaProjection */);
+            } else {
+                resultTo.addResultLocked(this, resultWho, requestCode, resultCode, resultData);
+            }
             resultTo = null;
         } else if (DEBUG_RESULTS) {
             Slog.v(TAG_RESULTS, "No result destination from " + this);
@@ -3451,6 +3458,10 @@
         }
     }
 
+    void setForceSendResultForMediaProjection() {
+        mForceSendResultForMediaProjection = true;
+    }
+
     private void prepareActivityHideTransitionAnimationIfOvarlay() {
         if (mTaskOverlay) {
             prepareActivityHideTransitionAnimation();
@@ -4541,6 +4552,12 @@
 
     void sendResult(int callingUid, String resultWho, int requestCode, int resultCode,
             Intent data, NeededUriGrants dataGrants) {
+        sendResult(callingUid, resultWho, requestCode, resultCode, data, dataGrants,
+                false /* forceSendForMediaProjection */);
+    }
+
+    private void sendResult(int callingUid, String resultWho, int requestCode, int resultCode,
+            Intent data, NeededUriGrants dataGrants, boolean forceSendForMediaProjection) {
         if (callingUid > 0) {
             mAtmService.mUgmInternal.grantUriPermissionUncheckedFromIntent(dataGrants,
                     getUriPermissionsLocked());
@@ -4549,8 +4566,10 @@
         if (DEBUG_RESULTS) {
             Slog.v(TAG, "Send activity result to " + this
                     + " : who=" + resultWho + " req=" + requestCode
-                    + " res=" + resultCode + " data=" + data);
+                    + " res=" + resultCode + " data=" + data
+                    + " forceSendForMediaProjection=" + forceSendForMediaProjection);
         }
+
         if (isState(RESUMED) && attachedToProcess()) {
             try {
                 final ArrayList<ResultInfo> list = new ArrayList<ResultInfo>();
@@ -4563,9 +4582,63 @@
             }
         }
 
+        // Schedule sending results now for Media Projection setup.
+        if (forceSendForMediaProjection && attachedToProcess() && isState(STARTED, PAUSING, PAUSED,
+                STOPPING, STOPPED)) {
+            final ClientTransaction transaction = ClientTransaction.obtain(app.getThread(), token);
+            // Build result to be returned immediately.
+            transaction.addCallback(ActivityResultItem.obtain(
+                    List.of(new ResultInfo(resultWho, requestCode, resultCode, data))));
+            // When the activity result is delivered, the activity will transition to RESUMED.
+            // Since the activity is only resumed so the result can be immediately delivered,
+            // return it to its original lifecycle state.
+            ActivityLifecycleItem lifecycleItem = getLifecycleItemForCurrentStateForResult();
+            if (lifecycleItem != null) {
+                transaction.setLifecycleStateRequest(lifecycleItem);
+            } else {
+                Slog.w(TAG, "Unable to get the lifecycle item for state " + mState
+                        + " so couldn't immediately send result");
+            }
+            try {
+                mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Exception thrown sending result to " + this, e);
+            }
+        }
+
         addResultLocked(null /* from */, resultWho, requestCode, resultCode, data);
     }
 
+    /**
+     * Provides a lifecycle item for the current stat. Only to be used when force sending an
+     * activity result (as part of MeidaProjection setup). Does not support the following states:
+     * {@link State#INITIALIZING}, {@link State#RESTARTING_PROCESS},
+     * {@link State#FINISHING}, {@link State#DESTROYING}, {@link State#DESTROYED}. It does not make
+     * sense to force send a result to an activity in these states. Does not support
+     * {@link State#RESUMED} since a resumed activity will end in the resumed state after handling
+     * the result.
+     *
+     * @return an {@link ActivityLifecycleItem} for the current state, or {@code null} if the
+     * state is not valid.
+     */
+    @Nullable
+    private ActivityLifecycleItem getLifecycleItemForCurrentStateForResult() {
+        switch (mState) {
+            case STARTED:
+                return StartActivityItem.obtain(null);
+            case PAUSING:
+            case PAUSED:
+                return PauseActivityItem.obtain();
+            case STOPPING:
+            case STOPPED:
+                return StopActivityItem.obtain(configChangeFlags);
+            default:
+                // Do not send a result immediately if the activity is in state INITIALIZING,
+                // RESTARTING_PROCESS, FINISHING, DESTROYING, or DESTROYED.
+                return null;
+        }
+    }
+
     private void addNewIntentLocked(ReferrerIntent intent) {
         if (newIntents == null) {
             newIntents = new ArrayList<>();
@@ -7898,7 +7971,7 @@
     }
 
     boolean isInTransition() {
-        return mTransitionController.inTransition() // Shell transitions.
+        return mTransitionController.inTransition(this) // Shell transitions.
                 || isAnimating(PARENTS | TRANSITION); // Legacy transitions.
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index d59f696..5d038dc 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -81,8 +81,8 @@
         // Don't block touches from passing through to an activity below us in the same task, if
         // that activity is either from the same uid or if that activity has launched an activity
         // in our uid.
-        final ActivityRecord activityBelowInTask =
-                mActivityRecord.getTask().getActivityBelow(mActivityRecord);
+        final ActivityRecord activityBelowInTask = mActivityRecord.getTask() != null
+                ? mActivityRecord.getTask().getActivityBelow(mActivityRecord) : null;
         final boolean allowPassthrough = activityBelowInTask != null && (
                 activityBelowInTask.mAllowedTouchUid == mActivityRecord.getUid()
                         || activityBelowInTask.isUid(mActivityRecord.getUid()));
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index a30c744..3bf1c51 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2501,11 +2501,6 @@
             }
         }
 
-        if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0 && mSourceRecord == null) {
-            // ignore the flag if there is no the sourceRecord
-            mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
-        }
-
         // We'll invoke onUserLeaving before onPause only if the launching
         // activity did not explicitly state that this is an automated launch.
         mSupervisor.mUserLeaving = (mLaunchFlags & FLAG_ACTIVITY_NO_USER_ACTION) == 0;
@@ -2700,6 +2695,12 @@
                 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
             }
         }
+
+        if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
+                && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {
+            // ignore the flag if there is no the sourceRecord or without new_task flag
+            mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
+        }
     }
 
     private void computeSourceRootTask() {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index b564aa6..7bc551b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -402,12 +402,25 @@
     /** The time at which the previous process was last visible. */
     private long mPreviousProcessVisibleTime;
 
+    /** It is set from keyguard-going-away to set-keyguard-shown. */
+    static final int DEMOTE_TOP_REASON_DURING_UNLOCKING = 1;
+    /** It is set if legacy recents animation is running. */
+    static final int DEMOTE_TOP_REASON_ANIMATING_RECENTS = 1 << 1;
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+            DEMOTE_TOP_REASON_DURING_UNLOCKING,
+            DEMOTE_TOP_REASON_ANIMATING_RECENTS,
+    })
+    @interface DemoteTopReason {}
+
     /**
-     * It can be true from keyguard-going-away to set-keyguard-shown. And getTopProcessState() will
+     * If non-zero, getTopProcessState() will
      * return {@link ActivityManager#PROCESS_STATE_IMPORTANT_FOREGROUND} to avoid top app from
-     * preempting CPU while keyguard is animating.
+     * preempting CPU while another process is running an important animation.
      */
-    private volatile boolean mDemoteTopAppDuringUnlocking;
+    @DemoteTopReason
+    volatile int mDemoteTopAppReasons;
 
     /** List of intents that were used to start the most recent tasks. */
     private RecentTasks mRecentTasks;
@@ -2841,8 +2854,8 @@
             }
             // Always reset the state regardless of keyguard-showing change, because that means the
             // unlock is either completed or canceled.
-            if (mDemoteTopAppDuringUnlocking) {
-                mDemoteTopAppDuringUnlocking = false;
+            if ((mDemoteTopAppReasons & DEMOTE_TOP_REASON_DURING_UNLOCKING) != 0) {
+                mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING;
                 // The scheduling group of top process was demoted by unlocking, so recompute
                 // to restore its real top priority if possible.
                 if (mTopApp != null) {
@@ -2883,7 +2896,7 @@
         // animation of system UI. Even if AOD is not enabled, it should be no harm.
         final WindowProcessController proc;
         synchronized (mGlobalLockWithoutBoost) {
-            mDemoteTopAppDuringUnlocking = false;
+            mDemoteTopAppReasons &= ~DEMOTE_TOP_REASON_DURING_UNLOCKING;
             final WindowState notificationShade = mRootWindowContainer.getDefaultDisplay()
                     .getDisplayPolicy().getNotificationShade();
             proc = notificationShade != null
@@ -3425,7 +3438,7 @@
                     mActivityClientController.invalidateHomeTaskSnapshot(null /* token */);
                 } else if (mKeyguardShown) {
                     // Only set if it is not unlocking to launcher which may also animate.
-                    mDemoteTopAppDuringUnlocking = true;
+                    mDemoteTopAppReasons |= DEMOTE_TOP_REASON_DURING_UNLOCKING;
                 }
 
                 mRootWindowContainer.forAllDisplays(displayContent -> {
@@ -4035,6 +4048,9 @@
             mTaskOrganizerController.dump(pw, "  ");
             mVisibleActivityProcessTracker.dump(pw, "  ");
             mActiveUids.dump(pw, "  ");
+            if (mDemoteTopAppReasons != 0) {
+                pw.println("  mDemoteTopAppReasons=" + mDemoteTopAppReasons);
+            }
         }
 
         if (!printedAnything) {
@@ -5663,8 +5679,8 @@
         @Override
         public int getTopProcessState() {
             final int topState = mTopProcessState;
-            if (mDemoteTopAppDuringUnlocking && topState == ActivityManager.PROCESS_STATE_TOP) {
-                // The unlocking UI is more important, so defer the top state of app.
+            if (mDemoteTopAppReasons != 0 && topState == ActivityManager.PROCESS_STATE_TOP) {
+                // There may be a more important UI/animation than the top app.
                 return ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
             }
             if (mRetainPowerModeAndTopProcessState) {
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index b519dad..e3de18b 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -18,7 +18,6 @@
 
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
 
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFORM;
 
@@ -152,8 +151,7 @@
     /** Assigns the operation for the window tokens which can update rotation asynchronously. */
     @Override
     public void accept(WindowState w) {
-        if (w.mActivityRecord != null || !w.mHasSurface || w.mIsWallpaper || w.mIsImWindow
-                || w.mAttrs.type == TYPE_NOTIFICATION_SHADE) {
+        if (!w.mHasSurface || !canBeAsync(w.mToken)) {
             return;
         }
         if (mTransitionOp == OP_LEGACY && w.mForceSeamlesslyRotate) {
@@ -185,23 +183,28 @@
         mTargetWindowTokens.put(w.mToken, new Operation(action));
     }
 
+    /** Returns {@code true} if the window token can update rotation independently. */
+    static boolean canBeAsync(WindowToken token) {
+        final int type = token.windowType;
+        return type > WindowManager.LayoutParams.LAST_APPLICATION_WINDOW
+                && type != WindowManager.LayoutParams.TYPE_INPUT_METHOD
+                && type != WindowManager.LayoutParams.TYPE_WALLPAPER
+                && type != WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
+    }
+
     /**
      * Enables {@link #handleFinishDrawing(WindowState, SurfaceControl.Transaction)} to capture the
      * draw transactions of the target windows if needed.
      */
     void keepAppearanceInPreviousRotation() {
+        if (mIsSyncDrawRequested) return;
         // The transition sync group may be finished earlier because it doesn't wait for these
         // target windows. But the windows still need to use sync transaction to keep the appearance
         // in previous rotation, so request a no-op sync to keep the state.
         for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
-            if (mHasScreenRotationAnimation
-                    && mTargetWindowTokens.valueAt(i).mAction == Operation.ACTION_FADE) {
-                // The windows are hidden (leash is alpha 0) before finishing drawing so it is
-                // unnecessary to request sync.
-                continue;
-            }
             final WindowToken token = mTargetWindowTokens.keyAt(i);
             for (int j = token.getChildCount() - 1; j >= 0; j--) {
+                // TODO(b/234585256): The consumer should be handleFinishDrawing().
                 token.getChildAt(j).applyWithNextDraw(t -> {});
             }
         }
@@ -387,6 +390,7 @@
      * transition starts. And associate transaction callback to consume pending animations.
      */
     void setupStartTransaction(SurfaceControl.Transaction t) {
+        if (mIsStartTransactionCommitted) return;
         for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
             final Operation op = mTargetWindowTokens.valueAt(i);
             final SurfaceControl leash = op.mLeash;
@@ -414,7 +418,6 @@
             }
         }
 
-        if (mIsStartTransactionCommitted) return;
         // If there are windows have redrawn in new rotation but the start transaction has not
         // been applied yet, the fade-in animation will be deferred. So once the transaction is
         // committed, the fade-in animation can run with screen rotation animation.
@@ -471,23 +474,18 @@
      * by this controller.
      */
     boolean handleFinishDrawing(WindowState w, SurfaceControl.Transaction postDrawTransaction) {
-        if (mTransitionOp == OP_LEGACY || postDrawTransaction == null
-                || !mIsSyncDrawRequested || !w.mTransitionController.inTransition()) {
+        if (mTransitionOp == OP_LEGACY || postDrawTransaction == null || !mIsSyncDrawRequested) {
             return false;
         }
         final Operation op = mTargetWindowTokens.get(w.mToken);
         if (op == null) return false;
-        final boolean keepUntilTransitionFinish =
-                mTransitionOp == OP_APP_SWITCH && op.mAction == Operation.ACTION_FADE;
-        final boolean keepUntilStartTransaction =
-                !mIsStartTransactionCommitted && op.mAction == Operation.ACTION_SEAMLESS;
-        if (!keepUntilTransitionFinish && !keepUntilStartTransaction) return false;
+        if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w);
         if (op.mDrawTransaction == null) {
             op.mDrawTransaction = postDrawTransaction;
         } else {
             op.mDrawTransaction.merge(postDrawTransaction);
         }
-        if (DEBUG) Slog.d(TAG, "Capture draw transaction " + w);
+        mDisplayContent.finishAsyncRotation(w.mToken);
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 16485f5..7eeda3f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1882,7 +1882,7 @@
     }
 
     /** Returns {@code true} if the decided new rotation has not applied to configuration yet. */
-    private boolean isRotationChanging() {
+    boolean isRotationChanging() {
         return mDisplayRotation.getRotation() != getWindowConfiguration().getRotation();
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index a78d25f..d2c71f5 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1635,7 +1635,9 @@
                 final WindowContainer<?> source = dc.getLastOrientationSource();
                 if (source != null) {
                     mLastOrientationSource = source.toString();
-                    mSourceOrientation = source.mOrientation;
+                    final WindowState w = source.asWindowState();
+                    mSourceOrientation =
+                            w != null ? w.mAttrs.screenOrientation : source.mOrientation;
                 } else {
                     mLastOrientationSource = null;
                     mSourceOrientation = SCREEN_ORIENTATION_UNSET;
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index eca201d..f840171 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -203,9 +203,7 @@
         final LaunchingState launchingState =
                 mTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunching(mTargetIntent);
 
-        if (mCaller != null) {
-            mCaller.setRunningRecentsAnimation(true);
-        }
+        setProcessAnimating(true);
 
         mService.deferWindowLayout();
         try {
@@ -409,15 +407,33 @@
                     if (mWindowManager.mRoot.isLayoutNeeded()) {
                         mWindowManager.mRoot.performSurfacePlacement();
                     }
-                    if (mCaller != null) {
-                        mCaller.setRunningRecentsAnimation(false);
-                    }
+                    setProcessAnimating(false);
                     Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
                 }
             });
         }
     }
 
+    /** Gives the owner of recents animation higher priority. */
+    private void setProcessAnimating(boolean animating) {
+        if (mCaller == null) return;
+        // Apply the top-app scheduling group to who runs the animation.
+        mCaller.setRunningRecentsAnimation(animating);
+        int demoteReasons = mService.mDemoteTopAppReasons;
+        if (animating) {
+            demoteReasons |= ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS;
+        } else {
+            demoteReasons &= ~ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS;
+        }
+        mService.mDemoteTopAppReasons = demoteReasons;
+        // Make the demotion of the real top app take effect. No need to restore top app state for
+        // finishing recents because addToStopping -> scheduleIdle -> activityIdleInternal ->
+        // trimApplications will have a full update.
+        if (animating && mService.mTopApp != null) {
+            mService.mTopApp.scheduleUpdateOomAdj();
+        }
+    }
+
     @Override
     public void onAnimationFinished(@RecentsAnimationController.ReorderMode int reorderMode,
             boolean sendUserLeaveHint) {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index d983572..02e7969 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -438,6 +438,14 @@
         if (!toTop) {
             if (t.mTaskId == mLastLeafTaskToFrontId) {
                 mLastLeafTaskToFrontId = INVALID_TASK_ID;
+
+                // If the previous front-most task is moved to the back, then notify of the new
+                // front-most task.
+                final ActivityRecord topMost = getTopMostActivity();
+                if (topMost != null) {
+                    mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(
+                            topMost.getTask().getTaskInfo());
+                }
             }
             return;
         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 84f61046..f163050 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -95,7 +95,6 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
 import java.util.function.Predicate;
 
 /**
@@ -656,6 +655,19 @@
                 mController.dispatchLegacyAppTransitionFinished(ar);
             }
         }
+
+        // Update the input-sink (touch-blocking) state now that the animation is finished.
+        SurfaceControl.Transaction inputSinkTransaction = null;
+        for (int i = 0; i < mParticipants.size(); ++i) {
+            final ActivityRecord ar = mParticipants.valueAt(i).asActivityRecord();
+            if (ar == null || !ar.isVisible()) continue;
+            if (inputSinkTransaction == null) {
+                inputSinkTransaction = new SurfaceControl.Transaction();
+            }
+            ar.mActivityRecordInputSink.applyChangesToSurfaceIfChanged(inputSinkTransaction);
+        }
+        if (inputSinkTransaction != null) inputSinkTransaction.apply();
+
         // Always schedule stop processing when transition finishes because activities don't
         // stop while they are in a transition thus their stop could still be pending.
         mController.mAtm.mTaskSupervisor
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index d725718..b579a9d 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3664,10 +3664,6 @@
             mChildren.get(i).finishSync(outMergedTransaction, cancel);
         }
         if (cancel && mSyncGroup != null) mSyncGroup.onCancelSync(this);
-        clearSyncState();
-    }
-
-    void clearSyncState() {
         mSyncState = SYNC_STATE_NONE;
         mSyncGroup = null;
     }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index fd0b933..7049960 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3894,6 +3894,8 @@
         mDragResizingChangeReported = true;
         mWindowFrames.clearReportResizeHints();
 
+        final int prevRotation = mLastReportedConfiguration
+                .getMergedConfiguration().windowConfiguration.getRotation();
         fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,
                 true /* useLatestConfig */, false /* relayoutVisible */);
         final boolean syncRedraw = shouldSendRedrawForSync();
@@ -3926,7 +3928,8 @@
             mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
                     getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
                     mSyncSeqId, resizeMode);
-            if (drawPending && mOrientationChanging) {
+            if (drawPending && prevRotation != mLastReportedConfiguration
+                    .getMergedConfiguration().windowConfiguration.getRotation()) {
                 mOrientationChangeRedrawRequestTime = SystemClock.elapsedRealtime();
                 ProtoLog.v(WM_DEBUG_ORIENTATION,
                         "Requested redraw for orientation change: %s", this);
@@ -5914,6 +5917,9 @@
 
     @Override
     boolean prepareSync() {
+        if (!mDrawHandlers.isEmpty()) {
+            Slog.w(TAG, "prepareSync with mDrawHandlers, " + this + ", " + Debug.getCallers(8));
+        }
         if (!super.prepareSync()) {
             return false;
         }
@@ -5975,11 +5981,9 @@
         if (asyncRotationController != null
                 && asyncRotationController.handleFinishDrawing(this, postDrawTransaction)) {
             // Consume the transaction because the controller will apply it with fade animation.
-            // Layout is not needed because the window will be hidden by the fade leash. Clear
-            // sync state because its sync transaction doesn't need to be merged to sync group.
+            // Layout is not needed because the window will be hidden by the fade leash.
             postDrawTransaction = null;
             skipLayout = true;
-            clearSyncState();
         } else if (onSyncFinishedDrawing() && postDrawTransaction != null) {
             mSyncTransaction.merge(postDrawTransaction);
             // Consume the transaction because the sync group will merge it.
@@ -6033,7 +6037,8 @@
         if (mRedrawForSyncReported) {
             return false;
         }
-        if (mInRelayout) {
+        // TODO(b/233286785): Remove mIsWallpaper once WallpaperService handles syncId of relayout.
+        if (mInRelayout && !mIsWallpaper) {
             // The last sync seq id will return to the client, so there is no need to request the
             // client to redraw.
             return false;
@@ -6070,6 +6075,10 @@
      * See {@link WindowState#mDrawHandlers}
      */
     void applyWithNextDraw(Consumer<SurfaceControl.Transaction> consumer) {
+        if (mSyncState != SYNC_STATE_NONE) {
+            Slog.w(TAG, "applyWithNextDraw with mSyncState=" + mSyncState + ", " + this
+                    + ", " + Debug.getCallers(8));
+        }
         mSyncSeqId++;
         mDrawHandlers.add(new DrawHandler(mSyncSeqId, consumer));
 
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 0fa8744..6c82a79 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -668,6 +668,15 @@
         }
     }
 
+    @Override
+    boolean prepareSync() {
+        if (mDisplayContent != null && mDisplayContent.isRotationChanging()
+                && AsyncRotationController.canBeAsync(this)) {
+            return false;
+        }
+        return super.prepareSync();
+    }
+
     @CallSuper
     @Override
     public void dumpDebug(ProtoOutputStream proto, long fieldId,
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 19f25c6..0331051 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1160,6 +1160,45 @@
         assertTrue(lastTransition.allReady());
     }
 
+    @Test
+    public void testFinishActivityIfPossible_sendResultImmediately() {
+        // Create activity representing the source of the activity result.
+        final ComponentName sourceComponent = ComponentName.createRelative(
+                DEFAULT_COMPONENT_PACKAGE_NAME, ".SourceActivity");
+        final ComponentName targetComponent = ComponentName.createRelative(
+                sourceComponent.getPackageName(), ".TargetActivity");
+
+        final ActivityRecord sourceActivity = new ActivityBuilder(mWm.mAtmService)
+                .setComponent(sourceComponent)
+                .setLaunchMode(ActivityInfo.LAUNCH_SINGLE_INSTANCE)
+                .setCreateTask(true)
+                .build();
+        sourceActivity.finishing = false;
+        sourceActivity.setState(STOPPED, "test");
+
+        final ActivityRecord targetActivity = new ActivityBuilder(mWm.mAtmService)
+                .setComponent(targetComponent)
+                .setTargetActivity(sourceComponent.getClassName())
+                .setLaunchMode(ActivityInfo.LAUNCH_SINGLE_INSTANCE)
+                .setCreateTask(true)
+                .setOnTop(true)
+                .build();
+        targetActivity.finishing = false;
+        targetActivity.setState(RESUMED, "test");
+        targetActivity.resultTo = sourceActivity;
+        targetActivity.setForceSendResultForMediaProjection();
+
+        clearInvocations(mAtm.getLifecycleManager());
+
+        targetActivity.finishIfPossible(0, new Intent(), null, "test", false /* oomAdj */);
+
+        try {
+            verify(mAtm.getLifecycleManager(), atLeastOnce()).scheduleTransaction(
+                    any(ClientTransaction.class));
+        } catch (RemoteException ignored) {
+        }
+    }
+
     /**
      * Verify that complete finish request for non-finishing activity is invalid.
      */
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
index 1b19a28..a1d6a50 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentsAnimationTest.java
@@ -37,13 +37,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 
-import android.app.IApplicationThread;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -91,9 +91,15 @@
         TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         Task recentsStack = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
                 ACTIVITY_TYPE_RECENTS, true /* onTop */);
+        final WindowProcessController wpc = mSystemServicesTestRule.addProcess(
+                mRecentsComponent.getPackageName(), mRecentsComponent.getPackageName(),
+                // Use real pid/uid of the test so the corresponding process can be mapped by
+                // Binder.getCallingPid/Uid.
+                WindowManagerService.MY_PID, WindowManagerService.MY_UID);
         ActivityRecord recentActivity = new ActivityBuilder(mAtm)
                 .setComponent(mRecentsComponent)
                 .setTask(recentsStack)
+                .setUseProcess(wpc)
                 .build();
         ActivityRecord topActivity = new ActivityBuilder(mAtm).setCreateTask(true).build();
         topActivity.getRootTask().moveToFront("testRecentsActivityVisiblility");
@@ -106,11 +112,14 @@
                 mRecentsComponent, true /* getRecentsAnimation */);
         // The launch-behind state should make the recents activity visible.
         assertTrue(recentActivity.mVisibleRequested);
+        assertEquals(ActivityTaskManagerService.DEMOTE_TOP_REASON_ANIMATING_RECENTS,
+                mAtm.mDemoteTopAppReasons);
 
         // Simulate the animation is cancelled without changing the stack order.
         recentsAnimation.onAnimationFinished(REORDER_KEEP_IN_PLACE, false /* sendUserLeaveHint */);
         // The non-top recents activity should be invisible by the restored launch-behind state.
         assertFalse(recentActivity.mVisibleRequested);
+        assertEquals(0, mAtm.mDemoteTopAppReasons);
     }
 
     @Test
@@ -138,11 +147,8 @@
                 anyInt() /* startFlags */, any() /* profilerInfo */);
 
         // Assume its process is alive because the caller should be the recents service.
-        WindowProcessController wpc = new WindowProcessController(mAtm, aInfo.applicationInfo,
-                aInfo.processName, aInfo.applicationInfo.uid, 0 /* userId */,
-                mock(Object.class) /* owner */, mock(WindowProcessListener.class));
-        wpc.setThread(mock(IApplicationThread.class));
-        doReturn(wpc).when(mAtm).getProcessController(eq(wpc.mName), eq(wpc.mUid));
+        mSystemServicesTestRule.addProcess(aInfo.packageName, aInfo.processName, 12345 /* pid */,
+                aInfo.applicationInfo.uid);
 
         Intent recentsIntent = new Intent().setComponent(mRecentsComponent);
         // Null animation indicates to preload.
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 18c4eb9..6c100d7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -43,12 +43,14 @@
 
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
+import android.app.IApplicationThread;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.database.ContentObserver;
@@ -430,6 +432,34 @@
                 .spiedInstance(sWakeLock).stubOnly());
     }
 
+    WindowProcessController addProcess(String pkgName, String procName, int pid, int uid) {
+        return addProcess(mAtmService, pkgName, procName, pid, uid);
+    }
+
+    static WindowProcessController addProcess(ActivityTaskManagerService atmService, String pkgName,
+            String procName, int pid, int uid) {
+        final ApplicationInfo info = new ApplicationInfo();
+        info.uid = uid;
+        info.packageName = pkgName;
+        return addProcess(atmService, info, procName, pid);
+    }
+
+    static WindowProcessController addProcess(ActivityTaskManagerService atmService,
+            ApplicationInfo info, String procName, int pid) {
+        final WindowProcessListener mockListener = mock(WindowProcessListener.class,
+                withSettings().stubOnly());
+        final int uid = info.uid;
+        final WindowProcessController proc = new WindowProcessController(atmService,
+                info, procName, uid, UserHandle.getUserId(uid), mockListener, mockListener);
+        proc.setThread(mock(IApplicationThread.class, withSettings().stubOnly()));
+        atmService.mProcessNames.put(procName, uid, proc);
+        if (pid > 0) {
+            proc.setPid(pid);
+            atmService.mProcessMap.put(pid, proc);
+        }
+        return proc;
+    }
+
     void cleanupWindowManagerHandlers() {
         final WindowManagerService wm = getWindowManagerService();
         if (wm == null) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 234bfa7..84e94ff 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -665,6 +665,11 @@
         mDisplayContent.setLastHasContent();
         mDisplayContent.requestChangeTransitionIfNeeded(1 /* any changes */,
                 null /* displayChange */);
+        assertEquals(WindowContainer.SYNC_STATE_NONE, statusBar.mSyncState);
+        assertEquals(WindowContainer.SYNC_STATE_NONE, navBar.mSyncState);
+        assertEquals(WindowContainer.SYNC_STATE_NONE, screenDecor.mSyncState);
+        assertEquals(WindowContainer.SYNC_STATE_WAITING_FOR_DRAW, ime.mSyncState);
+
         final AsyncRotationController asyncRotationController =
                 mDisplayContent.getAsyncRotationController();
         assertNotNull(asyncRotationController);
@@ -778,7 +783,7 @@
 
         player.start();
         player.finish();
-        app.getTask().clearSyncState();
+        app.getTask().finishSync(mWm.mTransactionFactory.get(), false /* cancel */);
 
         // The open transition is finished. Continue to play seamless display change transition,
         // so the previous async rotation controller should still exist.
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 50fa4cc..8a539cd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -64,7 +64,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
-import android.app.IApplicationThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -933,13 +932,15 @@
      */
     protected static class ActivityBuilder {
         static final int DEFAULT_FAKE_UID = 12345;
+        static final String DEFAULT_PROCESS_NAME = "procName";
+        static int sProcNameSeq;
 
         private final ActivityTaskManagerService mService;
 
         private ComponentName mComponent;
         private String mTargetActivity;
         private Task mTask;
-        private String mProcessName = "name";
+        private String mProcessName = DEFAULT_PROCESS_NAME;
         private String mAffinity;
         private int mUid = DEFAULT_FAKE_UID;
         private boolean mCreateTask = false;
@@ -1127,6 +1128,9 @@
             aInfo.applicationInfo.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
             aInfo.applicationInfo.packageName = mComponent.getPackageName();
             aInfo.applicationInfo.uid = mUid;
+            if (DEFAULT_PROCESS_NAME.equals(mProcessName)) {
+                mProcessName += ++sProcNameSeq;
+            }
             aInfo.processName = mProcessName;
             aInfo.packageName = mComponent.getPackageName();
             aInfo.name = mComponent.getClassName();
@@ -1191,16 +1195,11 @@
             if (mWpc != null) {
                 wpc = mWpc;
             } else {
-                wpc = new WindowProcessController(mService,
-                        aInfo.applicationInfo, mProcessName, mUid,
-                        UserHandle.getUserId(mUid), mock(Object.class),
-                        mock(WindowProcessListener.class));
-                wpc.setThread(mock(IApplicationThread.class));
+                final WindowProcessController p = mService.getProcessController(mProcessName, mUid);
+                wpc = p != null ? p : SystemServicesTestRule.addProcess(
+                        mService, aInfo.applicationInfo, mProcessName, 0 /* pid */);
             }
-            wpc.setThread(mock(IApplicationThread.class));
             activity.setProcess(wpc);
-            doReturn(wpc).when(mService).getProcessController(
-                    activity.processName, activity.info.applicationInfo.uid);
 
             // Resume top activities to make sure all other signals in the system are connected.
             mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
diff --git a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
index 15e5e64..eebcccb 100644
--- a/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
+++ b/services/usb/java/com/android/server/usb/UsbDirectMidiDevice.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
+import android.hardware.usb.UsbConfiguration;
 import android.hardware.usb.UsbDevice;
 import android.hardware.usb.UsbDeviceConnection;
 import android.hardware.usb.UsbEndpoint;
@@ -188,10 +189,36 @@
         // Otherwise, USB devices may not handle this gracefully.
         mShouldCallSetInterface = (parser.calculateMidiInterfaceDescriptorsCount() > 1);
 
+        ArrayList<UsbInterfaceDescriptor> midiInterfaceDescriptors;
         if (isUniversalMidiDevice) {
-            mUsbInterfaces = parser.findUniversalMidiInterfaceDescriptors();
+            midiInterfaceDescriptors = parser.findUniversalMidiInterfaceDescriptors();
         } else {
-            mUsbInterfaces = parser.findLegacyMidiInterfaceDescriptors();
+            midiInterfaceDescriptors = parser.findLegacyMidiInterfaceDescriptors();
+        }
+
+        mUsbInterfaces = new ArrayList<UsbInterfaceDescriptor>();
+        if (mUsbDevice.getConfigurationCount() > 0) {
+            // USB devices should default to the first configuration.
+            // The first configuration should support MIDI.
+            // Only one configuration can be used at once.
+            // Thus, use USB interfaces from the first configuration.
+            UsbConfiguration usbConfiguration = mUsbDevice.getConfiguration(0);
+            for (int interfaceIndex = 0; interfaceIndex < usbConfiguration.getInterfaceCount();
+                    interfaceIndex++) {
+                UsbInterface usbInterface = usbConfiguration.getInterface(interfaceIndex);
+                for (UsbInterfaceDescriptor midiInterfaceDescriptor : midiInterfaceDescriptors) {
+                    UsbInterface midiInterface = midiInterfaceDescriptor.toAndroid(mParser);
+                    if (areEquivalent(usbInterface, midiInterface)) {
+                        mUsbInterfaces.add(midiInterfaceDescriptor);
+                        break;
+                    }
+                }
+            }
+
+            if (mUsbDevice.getConfigurationCount() > 1) {
+                Log.w(TAG, "Skipping some USB configurations. Count: "
+                        + mUsbDevice.getConfigurationCount());
+            }
         }
 
         int numInputs = 0;
@@ -647,6 +674,37 @@
         return true;
     }
 
+    private boolean areEquivalent(UsbInterface interface1, UsbInterface interface2) {
+        if ((interface1.getId() != interface2.getId())
+                || (interface1.getAlternateSetting() != interface2.getAlternateSetting())
+                || (interface1.getInterfaceClass() != interface2.getInterfaceClass())
+                || (interface1.getInterfaceSubclass() != interface2.getInterfaceSubclass())
+                || (interface1.getInterfaceProtocol() != interface2.getInterfaceProtocol())
+                || (interface1.getEndpointCount() != interface2.getEndpointCount())) {
+            return false;
+        }
+
+        if (interface1.getName() == null) {
+            if (interface2.getName() != null) {
+                return false;
+            }
+        } else if (!(interface1.getName().equals(interface2.getName()))) {
+            return false;
+        }
+
+        // Consider devices with the same endpoints but in a different order as different endpoints.
+        for (int i = 0; i < interface1.getEndpointCount(); i++) {
+            UsbEndpoint endpoint1 = interface1.getEndpoint(i);
+            UsbEndpoint endpoint2 = interface2.getEndpoint(i);
+            if ((endpoint1.getAddress() != endpoint2.getAddress())
+                    || (endpoint1.getAttributes() != endpoint2.getAttributes())
+                    || (endpoint1.getMaxPacketSize() != endpoint2.getMaxPacketSize())
+                    || (endpoint1.getInterval() != endpoint2.getInterval())) {
+                return false;
+            }
+        }
+        return true;
+    }
     /**
      * Write a description of the device to a dump stream.
      */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
index 7f309e1..0e5a177 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt
@@ -18,6 +18,7 @@
 package com.android.server.wm.flicker
 
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.traces.region.RegionSubject
 import com.android.server.wm.traces.common.FlickerComponentName
 
 val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher",
@@ -173,6 +174,33 @@
 }
 
 /**
+ * Asserts that the visibleRegion of the [FlickerComponentName.SNAPSHOT] layer can cover
+ * the visibleRegion of the given app component exactly
+ */
+fun FlickerTestParameter.snapshotStartingWindowLayerCoversExactlyOnApp(
+        component: FlickerComponentName) {
+    assertLayers {
+        invoke("snapshotStartingWindowLayerCoversExactlyOnApp") {
+            val snapshotLayers = it.subjects.filter { subject ->
+                subject.name.contains(
+                        FlickerComponentName.SNAPSHOT.toLayerName()) && subject.isVisible
+            }
+            // Verify the size of snapshotRegion covers appVisibleRegion exactly in animation.
+            if (snapshotLayers.isNotEmpty()) {
+                val visibleAreas = snapshotLayers.mapNotNull { snapshotLayer ->
+                    snapshotLayer.layer?.visibleRegion
+                }.toTypedArray()
+                val snapshotRegion = RegionSubject.assertThat(visibleAreas, this, timestamp)
+                val appVisibleRegion = it.visibleRegion(component)
+                if (snapshotRegion.region.isNotEmpty) {
+                    snapshotRegion.coversExactly(appVisibleRegion.region)
+                }
+            }
+        }
+    }
+}
+
+/**
  * Asserts that:
  *     [originalLayer] is visible at the start of the trace
  *     [originalLayer] becomes invisible during the trace and (in the same entry) [newLayer]
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
index 933a3a0..88fb1a2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker.ime
 
 import android.app.Instrumentation
+import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.Presubmit
 import android.platform.test.annotations.RequiresDevice
 import android.view.Surface
@@ -34,6 +35,7 @@
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarLayerPositionEnd
 import com.android.server.wm.flicker.navBarWindowIsVisible
+import com.android.server.wm.flicker.snapshotStartingWindowLayerCoversExactlyOnApp
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import org.junit.FixMethodOrder
@@ -107,6 +109,12 @@
     @Test
     fun imeLayerBecomesVisible() = testSpec.imeLayerBecomesVisible()
 
+    @Postsubmit
+    @Test
+    fun snapshotStartingWindowLayerCoversExactlyOnApp() {
+        testSpec.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp.component)
+    }
+
     companion object {
         /**
          * Creates the test configurations.