Restart running task if another task was started but not appeared

If we just finish the controller to resume the running task like
we normally would, the delayed task will appear on top when it's
ready. Instead, we need to start the original task in this case.

- Move mLastStartedTaskId to GestureState, shared between swipe
  handlers when continuing the gesture.
- Update logic to change LAST_TASK to NEW_TASK if a task was
  started (but not appeared) during the same gesture; likewise,
  only change NEW_TASK to LAST_TASK if no task was started.
- Finish the controller when successfully starting the task
  that already appeared, since we won't get onTaskAppeared().

Bug: 156132424
Change-Id: I1e9af297840745ab3d5e90214425f10a2616d90a
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index eb976c7..dd41d25 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -40,6 +40,7 @@
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
 
+import androidx.annotation.CallSuper;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -128,7 +129,6 @@
     protected MultiStateCallback mStateCallback;
 
     protected boolean mCanceled;
-    protected int mLastStartedTaskId = -1;
 
     private boolean mRecentsViewScrollLinked = false;
 
@@ -218,11 +218,16 @@
             if (!mCanceled) {
                 TaskView nextTask = mRecentsView.getTaskView(taskId);
                 if (nextTask != null) {
-                    mLastStartedTaskId = taskId;
+                    mGestureState.updateLastStartedTaskId(taskId);
                     nextTask.launchTask(false /* animate */, true /* freezeTaskList */,
                             success -> {
                                 resultCallback.accept(success);
-                                if (!success) {
+                                if (success) {
+                                    if (mRecentsView.indexOfChild(nextTask)
+                                            == getLastAppearedTaskIndex()) {
+                                        onRestartLastAppearedTask();
+                                    }
+                                } else {
                                     mActivityInterface.onLaunchTaskFailed();
                                     nextTask.notifyTaskLaunchFailed(TAG);
                                     mRecentsAnimationController.finish(true /* toRecents */, null);
@@ -236,6 +241,19 @@
     }
 
     /**
+     * Called when we successfully startNewTask() on the task that was previously running. Normally
+     * we call resumeLastTask() when returning to the previously running task, but this handles a
+     * specific edge case: if we switch from A to B, and back to A before B appears, we need to
+     * start A again to ensure it stays on top.
+     */
+    @CallSuper
+    protected void onRestartLastAppearedTask() {
+        // Finish the controller here, since we won't get onTaskAppeared() for a task that already
+        // appeared.
+        mRecentsAnimationController.finish(false, null);
+    }
+
+    /**
      * Runs the given {@param action} if the recents animation has already started, or queues it to
      * be run when it is next started.
      */
@@ -331,6 +349,14 @@
                 : mRecentsView.getRunningTaskIndex();
     }
 
+    /**
+     * @return Whether we are continuing a gesture that already landed on a new task,
+     * but before that task appeared.
+     */
+    protected boolean hasStartedNewTask() {
+        return mGestureState.getLastStartedTaskId() != -1;
+    }
+
     protected void initTransitionEndpoints(DeviceProfile dp) {
         mDp = dp;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 21c8c20..e1f34ed 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -725,7 +725,8 @@
         if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) {
             return false;
         }
-        if (appearedTaskTarget.taskId == mLastStartedTaskId) {
+        if (mGestureState.getEndTarget() == NEW_TASK
+                && appearedTaskTarget.taskId == mGestureState.getLastStartedTaskId()) {
             reset();
             return true;
         }
@@ -1016,17 +1017,19 @@
                     if (mRecentsView != null) {
                         int taskToLaunch = mRecentsView.getNextPage();
                         int runningTask = getLastAppearedTaskIndex();
-                        if (target == NEW_TASK && taskToLaunch == runningTask) {
+                        boolean hasStartedNewTask = hasStartedNewTask();
+                        if (target == NEW_TASK && taskToLaunch == runningTask
+                                && !hasStartedNewTask) {
                             // We are about to launch the current running task, so use LAST_TASK
                             // state instead of NEW_TASK. This could happen, for example, if our
                             // scroll is aborted after we determined the target to be NEW_TASK.
                             mGestureState.setEndTarget(LAST_TASK);
-                        } else if (target == LAST_TASK && taskToLaunch != runningTask) {
+                        } else if (target == LAST_TASK && hasStartedNewTask) {
                             // We are about to re-launch the previously running task, but we can't
                             // just finish the controller like we normally would because that would
-                            // instead resume the last task that appeared. As a workaround, launch
-                            // the task as if it were a new task.
-                            // TODO: is this expected?
+                            // instead resume the last task that appeared, and not ensure that this
+                            // task is restored to the top. To address this, re-launch the task as
+                            // if it were a new task.
                             mGestureState.setEndTarget(NEW_TASK);
                         }
                     }
@@ -1164,6 +1167,12 @@
         });
     }
 
+    @Override
+    protected void onRestartLastAppearedTask() {
+        super.onRestartLastAppearedTask();
+        reset();
+    }
+
     private void reset() {
         mStateCallback.setStateOnUiThread(STATE_HANDLER_INVALIDATED);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
index c44d9d7..4b2fc75 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/TouchInteractionService.java
@@ -518,6 +518,7 @@
                 ActiveGestureLog.INSTANCE.generateAndSetLogId());
         if (mTaskAnimationManager.isRecentsAnimationRunning()) {
             gestureState.updateRunningTask(mGestureState.getRunningTask());
+            gestureState.updateLastStartedTaskId(mGestureState.getLastStartedTaskId());
         } else {
             gestureState.updateRunningTask(TraceHelper.whitelistIpcs("getRunningTask.0",
                     () -> mAM.getRunningTask(false /* filterOnlyVisibleRecents */)));
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index f06e1a6..9b515ae 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -15,7 +15,6 @@
  */
 package com.android.quickstep;
 
-import static com.android.quickstep.GestureState.GestureEndTarget.NEW_TASK;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 
 import android.app.ActivityManager;
@@ -121,6 +120,7 @@
     private ActivityManager.RunningTaskInfo mRunningTask;
     private GestureEndTarget mEndTarget;
     private RemoteAnimationTargetCompat mLastAppearedTaskTarget;
+    private int mLastStartedTaskId = -1;
 
     public GestureState(OverviewComponentObserver componentObserver, int gestureId) {
         mHomeIntent = componentObserver.getHomeIntent();
@@ -139,6 +139,7 @@
         mRunningTask = other.mRunningTask;
         mEndTarget = other.mEndTarget;
         mLastAppearedTaskTarget = other.mLastAppearedTaskTarget;
+        mLastStartedTaskId = other.mLastStartedTaskId;
     }
 
     public GestureState() {
@@ -235,6 +236,21 @@
     }
 
     /**
+     * Updates the last task that we started via startActivityFromRecents() during this gesture.
+     */
+    public void updateLastStartedTaskId(int lastStartedTaskId) {
+        mLastStartedTaskId = lastStartedTaskId;
+    }
+
+    /**
+     * @return The id of the task that was most recently started during this gesture, or -1 if
+     * no task has been started yet (i.e. we haven't settled on a new task).
+     */
+    public int getLastStartedTaskId() {
+        return mLastStartedTaskId;
+    }
+
+    /**
      * @return the end target for this gesture (if known).
      */
     public GestureEndTarget getEndTarget() {
@@ -300,6 +316,7 @@
         pw.println("  runningTask=" + mRunningTask);
         pw.println("  endTarget=" + mEndTarget);
         pw.println("  lastAppearedTaskTarget=" + mLastAppearedTaskTarget);
+        pw.println("  lastStartedTaskId=" + mLastStartedTaskId);
         pw.println("  isRecentsAnimationRunning=" + isRecentsAnimationRunning());
     }
 }