Block relayout during Recents transition

The Recents transition takes ownership of the task leash to reposition
it and scale it if needed for the overview animation. During this time,
a focus change may trigger TaskListener#onTaskInfoChanged which causes
a window decor relayout that applies an incorrect position and crop.

Bug: 296921174
Test: swipe up from taskbar, verify freeform task doesn't jump out of
position and gets a bad crop applied.
Test: atest DesktopModeWindowDecorViewModel

Change-Id: If6af9ab76e9c7174a9bb8c9c97e4a79a9b52a775
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 fd23d14..9f9854e 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
@@ -205,6 +205,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
+            RecentsTransitionHandler recentsTransitionHandler,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         if (DesktopModeStatus.isEnabled()) {
             return new DesktopModeWindowDecorViewModel(
@@ -218,6 +219,7 @@
                     syncQueue,
                     transitions,
                     desktopTasksController,
+                    recentsTransitionHandler,
                     rootTaskDisplayAreaOrganizer);
         }
         return new CaptionWindowDecorViewModel(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 7ae0666..0e617ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -134,6 +134,9 @@
         }
         final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
                 mixedHandler == null ? this : mixedHandler);
+        for (int i = 0; i < mStateListeners.size(); i++) {
+            mStateListeners.get(i).onTransitionStarted(transition);
+        }
         if (mixer != null) {
             mixer.setRecentsTransition(transition);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
index 804dcc8..e8733eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionStateListener.java
@@ -16,10 +16,15 @@
 
 package com.android.wm.shell.recents;
 
+import android.os.IBinder;
+
 /** The listener for the events from {@link RecentsTransitionHandler}. */
 public interface RecentsTransitionStateListener {
 
     /** Notifies whether the recents animation is running. */
     default void onAnimationStateChanged(boolean running) {
     }
+
+    /** Notifies that a recents shell transition has started. */
+    default void onTransitionStarted(IBinder transition) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index b4e1818..949ee81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -21,6 +21,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE;
@@ -73,6 +74,8 @@
 import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.RecentsTransitionStateListener;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
@@ -102,6 +105,7 @@
     private final DisplayController mDisplayController;
     private final SyncTransactionQueue mSyncQueue;
     private final Optional<DesktopTasksController> mDesktopTasksController;
+    private final RecentsTransitionHandler mRecentsTransitionHandler;
     private boolean mTransitionDragActive;
 
     private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -135,6 +139,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
+            RecentsTransitionHandler recentsTransitionHandler,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer
     ) {
         this(
@@ -148,6 +153,7 @@
                 syncQueue,
                 transitions,
                 desktopTasksController,
+                recentsTransitionHandler,
                 new DesktopModeWindowDecoration.Factory(),
                 new InputMonitorFactory(),
                 SurfaceControl.Transaction::new,
@@ -167,6 +173,7 @@
             SyncTransactionQueue syncQueue,
             Transitions transitions,
             Optional<DesktopTasksController> desktopTasksController,
+            RecentsTransitionHandler recentsTransitionHandler,
             DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
             InputMonitorFactory inputMonitorFactory,
             Supplier<SurfaceControl.Transaction> transactionFactory,
@@ -182,6 +189,7 @@
         mSyncQueue = syncQueue;
         mTransitions = transitions;
         mDesktopTasksController = desktopTasksController;
+        mRecentsTransitionHandler = recentsTransitionHandler;
 
         mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
         mInputMonitorFactory = inputMonitorFactory;
@@ -194,6 +202,12 @@
 
     private void onInit() {
         mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener);
+        mRecentsTransitionHandler.addTransitionStateListener(new RecentsTransitionStateListener() {
+            @Override
+            public void onTransitionStarted(IBinder transition) {
+                onRecentsTransitionStarted(transition);
+            }
+        });
     }
 
     @Override
@@ -319,6 +333,16 @@
         }
     }
 
+    private void onRecentsTransitionStarted(IBinder transition) {
+        // Block relayout on window decorations originating from #onTaskInfoChanges until the
+        // animation completes to avoid interfering with the transition animation.
+        for (int i = 0; i < mWindowDecorByTaskId.size(); i++) {
+            final DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i);
+            decor.incrementRelayoutBlock();
+            decor.addTransitionPausingRelayout(transition);
+        }
+    }
+
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
             DragDetector.MotionEventHandler{
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
index d8afe68..249f23a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java
@@ -22,6 +22,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -38,6 +39,7 @@
 import android.hardware.display.VirtualDisplay;
 import android.hardware.input.InputManager;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Looper;
 import android.view.Choreographer;
 import android.view.Display;
@@ -54,14 +56,18 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.RecentsTransitionStateListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 
 import java.util.ArrayList;
@@ -96,18 +102,21 @@
     @Mock private SurfaceControl.Transaction mTransaction;
     @Mock private Display mDisplay;
     @Mock private ShellController mShellController;
-    @Mock private ShellInit mShellInit;
+    @Mock private ShellExecutor mShellExecutor;
     @Mock private DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
             mDesktopModeKeyguardChangeListener;
     @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+    @Mock private RecentsTransitionHandler mRecentsTransitionHandler;
+
     private final List<InputManager> mMockInputManagers = new ArrayList<>();
 
+    private ShellInit mShellInit;
     private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel;
 
     @Before
     public void setUp() {
         mMockInputManagers.add(mInputManager);
-
+        mShellInit = new ShellInit(mShellExecutor);
         mDesktopModeWindowDecorViewModel =
                 new DesktopModeWindowDecorViewModel(
                         mContext,
@@ -120,6 +129,7 @@
                         mSyncQueue,
                         mTransitions,
                         Optional.of(mDesktopTasksController),
+                        mRecentsTransitionHandler,
                         mDesktopModeWindowDecorFactory,
                         mMockInputMonitorFactory,
                         mTransactionFactory,
@@ -143,6 +153,8 @@
 
         mDesktopModeWindowDecoration.mDisplay = mDisplay;
         doReturn(Display.DEFAULT_DISPLAY).when(mDisplay).getDisplayId();
+
+        mShellInit.init();
     }
 
     @Test
@@ -296,6 +308,36 @@
                 .create(any(), any(), any(), any(), any(), any(), any(), any(), any());
     }
 
+    @Test
+    public void testRelayoutBlockedDuringRecentsTransition() throws Exception {
+        final ArgumentCaptor<RecentsTransitionStateListener> recentsCaptor =
+                ArgumentCaptor.forClass(RecentsTransitionStateListener.class);
+        verify(mRecentsTransitionHandler).addTransitionStateListener(recentsCaptor.capture());
+
+        final IBinder transition = mock(IBinder.class);
+        final DesktopModeWindowDecoration decoration = mock(DesktopModeWindowDecoration.class);
+        final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+        final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+        final int taskId = 1;
+        final SurfaceControl taskSurface = new SurfaceControl();
+        final ActivityManager.RunningTaskInfo taskInfo =
+                createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM);
+        doReturn(decoration).when(mDesktopModeWindowDecorFactory)
+                .create(any(), any(), any(), eq(taskInfo), eq(taskSurface), any(), any(), any(),
+                        any());
+
+        runOnMainThread(() -> {
+            // Make sure a window decorations exists first by launching a freeform task.
+            mDesktopModeWindowDecorViewModel.onTaskOpening(
+                    taskInfo, taskSurface, startT, finishT);
+            // Now call back when as a Recents transition starts.
+            recentsCaptor.getValue().onTransitionStarted(transition);
+        });
+
+        verify(decoration).incrementRelayoutBlock();
+        verify(decoration).addTransitionPausingRelayout(transition);
+    }
+
     private void runOnMainThread(Runnable r) throws Exception {
         final Handler mainHandler = new Handler(Looper.getMainLooper());
         final CountDownLatch latch = new CountDownLatch(1);