Forcefully finish recents animations when launcher is detroyed

If launcher is destroyed while the recents animation start is pending, then the taskanimationmanager and absswipeuphandler states are not properly cleaned up. Adding a new cleanup flow to handle this case.

Flag: EXEMPT bug fix
Fixes: 405642423
Test: adb shell cmd uimode night yes/no while TaskAnimationManager.mRecentsAnimationStartPending == true
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:8d72503263e8108aa78a527dde1487eb60c867f6)
Merged-In: I7bf1fc4fc07859f92d7aec6cd78deafa1214dd17
Change-Id: I7bf1fc4fc07859f92d7aec6cd78deafa1214dd17
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 48630b1..f803709 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -221,15 +221,6 @@
     // The previous task view type before the user quick switches between tasks
     private TaskViewType mPreviousTaskViewType;
 
-    private final Runnable mLauncherOnDestroyCallback = () -> {
-        ActiveGestureProtoLogProxy.logLauncherDestroyed();
-        mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
-        mRecentsView = null;
-        mContainer = null;
-        mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
-        mRecentsAnimationStartCallbacks.clear();
-    };
-
     private static int FLAG_COUNT = 0;
     private static int getNextStateFlag(String name) {
         if (DEBUG_STATES) {
@@ -356,6 +347,16 @@
     private final SwipePipToHomeAnimator[] mSwipePipToHomeAnimators =
             new SwipePipToHomeAnimator[2];
 
+    private final Runnable mLauncherOnDestroyCallback = () -> {
+        ActiveGestureProtoLogProxy.logLauncherDestroyed();
+        mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener);
+        mRecentsView = null;
+        mContainer = null;
+        mStateCallback.clearState(STATE_LAUNCHER_PRESENT);
+        mRecentsAnimationStartCallbacks.clear();
+        mTaskAnimationManager.onLauncherDestroyed();
+    };
+
     // Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
     private final float mQuickSwitchScaleScrollThreshold;
 
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index cf0a3d5..e552cd9 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -425,22 +425,29 @@
     public void finishRunningRecentsAnimation(boolean toHome) {
         finishRunningRecentsAnimation(toHome, false /* forceFinish */, null /* forceFinishCb */);
     }
+    public void finishRunningRecentsAnimation(
+            boolean toHome, boolean forceFinish, Runnable forceFinishCb) {
+        finishRunningRecentsAnimation(toHome, forceFinish, forceFinishCb, mController);
+    }
 
     /**
      * Finishes the running recents animation.
      * @param forceFinish will synchronously finish the controller
      */
-    public void finishRunningRecentsAnimation(boolean toHome, boolean forceFinish,
-            Runnable forceFinishCb) {
-        if (mController != null) {
+    public void finishRunningRecentsAnimation(
+            boolean toHome,
+            boolean forceFinish,
+            @Nullable Runnable forceFinishCb,
+            @Nullable RecentsAnimationController controller) {
+        if (controller != null) {
             ActiveGestureProtoLogProxy.logFinishRunningRecentsAnimation(toHome);
             if (forceFinish) {
-                mController.finishController(toHome, forceFinishCb, false /* sendUserLeaveHint */,
+                controller.finishController(toHome, forceFinishCb, false /* sendUserLeaveHint */,
                         true /* forceFinish */);
             } else {
                 Utilities.postAsyncCallback(MAIN_EXECUTOR.getHandler(), toHome
-                        ? mController::finishAnimationToHome
-                        : mController::finishAnimationToApp);
+                        ? controller::finishAnimationToHome
+                        : controller::finishAnimationToApp);
             }
         }
     }
@@ -465,6 +472,29 @@
         return mController != null;
     }
 
+    void onLauncherDestroyed() {
+        if (!mRecentsAnimationStartPending) {
+            return;
+        }
+        if (mCallbacks == null) {
+            return;
+        }
+        ActiveGestureProtoLogProxy.logQueuingForceFinishRecentsAnimation();
+        mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
+            @Override
+            public void onRecentsAnimationStart(
+                    RecentsAnimationController controller,
+                    RecentsAnimationTargets targets,
+                    @Nullable TransitionInfo transitionInfo) {
+                finishRunningRecentsAnimation(
+                        /* toHome= */ false,
+                        /* forceFinish= */ true,
+                        /* forceFinishCb= */ null,
+                        controller);
+            }
+        });
+    }
+
     /**
      * Cleans up the recents animation entirely.
      */
diff --git a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
index 2532fcf..1c8656c 100644
--- a/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
+++ b/quickstep/src_protolog/com/android/quickstep/util/ActiveGestureProtoLogProxy.java
@@ -570,4 +570,13 @@
                 "OtherActivityInputConsumer.startTouchTrackingForWindowAnimation: "
                         + "interactionHandler=%s", interactionHandler);
     }
+
+    public static void logQueuingForceFinishRecentsAnimation() {
+        ActiveGestureLog.INSTANCE.addLog("Launcher destroyed while mRecentsAnimationStartPending =="
+                        + " true, queuing a callback to clean the pending animation up on start",
+                /* gestureEvent= */ ON_START_RECENTS_ANIMATION);
+        if (!enableActiveGestureProtoLog() || !isProtoLogInitialized()) return;
+        ProtoLog.d(ACTIVE_GESTURE_LOG, "Launcher destroyed while mRecentsAnimationStartPending =="
+                + " true, queuing a callback to clean the pending animation up on start");
+    }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
index fd88a5c..75b59d7 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/TaskAnimationManagerTest.java
@@ -19,19 +19,30 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.os.Bundle;
 import android.view.Display;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -80,6 +91,56 @@
                 optionsCaptor.getValue().getPendingIntentBackgroundActivityStartMode());
     }
 
+    @Test
+    public void testLauncherDestroyed_whileRecentsAnimationStartPending_finishesAnimation() {
+        final GestureState gestureState = mock(GestureState.class);
+        final ArgumentCaptor<RecentsAnimationCallbacks> listenerCaptor =
+                ArgumentCaptor.forClass(RecentsAnimationCallbacks.class);
+        final RecentsAnimationControllerCompat controllerCompat =
+                mock(RecentsAnimationControllerCompat.class);
+        final RemoteAnimationTarget remoteAnimationTarget = new RemoteAnimationTarget(
+                /* taskId= */ 0,
+                /* mode= */ RemoteAnimationTarget.MODE_CLOSING,
+                /* leash= */ new SurfaceControl(),
+                /* isTranslucent= */ false,
+                /* clipRect= */ null,
+                /* contentInsets= */ null,
+                /* prefixOrderIndex= */ 0,
+                /* position= */ null,
+                /* localBounds= */ null,
+                /* screenSpaceBounds= */ null,
+                new Configuration().windowConfiguration,
+                /* isNotInRecents= */ false,
+                /* startLeash= */ null,
+                /* startBounds= */ null,
+                /* taskInfo= */ new ActivityManager.RunningTaskInfo(),
+                /* allowEnterPip= */ false);
+
+        doReturn(mock(LauncherActivityInterface.class)).when(gestureState).getContainerInterface();
+        when(mSystemUiProxy
+                .startRecentsActivity(any(), any(), listenerCaptor.capture(), anyBoolean()))
+                .thenReturn(true);
+        when(gestureState.getRunningTaskIds(anyBoolean())).thenReturn(new int[0]);
+
+        runOnMainSync(() -> {
+            mTaskAnimationManager.startRecentsAnimation(
+                    gestureState,
+                    new Intent(),
+                    mock(RecentsAnimationCallbacks.RecentsAnimationListener.class));
+            mTaskAnimationManager.onLauncherDestroyed();
+            listenerCaptor.getValue().onAnimationStart(
+                    controllerCompat,
+                    new RemoteAnimationTarget[] { remoteAnimationTarget },
+                    new RemoteAnimationTarget[] { remoteAnimationTarget },
+                    new Rect(),
+                    new Rect(),
+                    new Bundle(),
+                    new TransitionInfo(0, 0));
+        });
+        runOnMainSync(() -> verify(controllerCompat)
+                .finish(/* toHome= */ eq(false), anyBoolean(), any()));
+    }
+
     protected static void runOnMainSync(Runnable runnable) {
         InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
     }