Merge "Try to detect and handle delayed quickswitch task launch failure" into udc-dev
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index ed78e2d..75cfd05 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -207,6 +207,10 @@
                     com.android.launcher3.taskbar.Utilities.setOverviewDragState(
                             mControllers, finalState.disallowTaskbarGlobalDrag(),
                             disallowLongClick, finalState.allowTaskbarInitialSplitSelection());
+                    // LauncherTaskbarUIController depends on the state when checking whether
+                    // to handle resume, so it should also be poked if current state changes
+                    mLauncher.getTaskbarUIController().onLauncherResumedOrPaused(
+                            mLauncher.hasBeenResumed());
                 }
             };
 
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 913f08f..d798e62 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -145,6 +145,9 @@
      * @param filter Returns true if GroupTask should be in the list of considerations
      */
     public void isTaskRemoved(int taskId, Consumer<Boolean> callback, Predicate<GroupTask> filter) {
+        // Invalidate the existing list before checking to ensure this reflects the current state in
+        // the system
+        mTaskList.onRecentTasksChanged();
         mTaskList.getTasks(true /* loadKeysOnly */, (taskGroups) -> {
             for (GroupTask group : taskGroups) {
                 if (group.containsTask(taskId)) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 4b1dd43..074aedd 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -79,7 +79,7 @@
     }
 
     @Override
-    public void startHome() {
+    public void startHome(boolean animated) {
         mActivity.startHome();
         AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
new file mode 100644
index 0000000..c22e0bc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
+import com.android.quickstep.RecentsModel;
+
+/**
+ * This class tracks the failure of a task launch through the TaskView.launchTask() call, in an
+ * edge case in which starting a new task may initially succeed (startActivity returns true), but
+ * the launch ultimately fails if the activity finishes while it is resuming.
+ *
+ * There are two signals this class checks, the launcher lifecycle and the transition completion.
+ * If we hit either of those signals and the task is no longer valid, then the registered failure
+ * callback will be notified.
+ */
+public class TaskRemovedDuringLaunchListener implements ActivityLifecycleCallbacksAdapter {
+
+    private Activity mActivity;
+    private int mLaunchedTaskId = INVALID_TASK_ID;
+    private Runnable mTaskLaunchFailedCallback = null;
+
+    /**
+     * Registers a failure listener callback if it detects a scenario in which an app launch
+     * failed before the transition finished.
+     */
+    public void register(Activity activity, int launchedTaskId,
+            @NonNull Runnable taskLaunchFailedCallback) {
+        activity.registerActivityLifecycleCallbacks(this);
+        mActivity = activity;
+        mLaunchedTaskId = launchedTaskId;
+        mTaskLaunchFailedCallback = taskLaunchFailedCallback;
+    }
+
+    /**
+     * Unregisters the failure listener.
+     */
+    private void unregister() {
+        mActivity.unregisterActivityLifecycleCallbacks(this);
+        mActivity = null;
+        mLaunchedTaskId = INVALID_TASK_ID;
+        mTaskLaunchFailedCallback = null;
+    }
+
+    /**
+     * Called when the transition finishes.
+     */
+    public void onTransitionFinished() {
+        // The transition finished and Launcher was not stopped, check if the launch failed
+        checkTaskLaunchFailed();
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+        // The normal task launch case, Launcher stops and updates its state correctly
+        unregister();
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+        // The transition hasn't finished but Launcher was resumed, check if the launch failed
+        checkTaskLaunchFailed();
+    }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+        // If we somehow don't get any of the above signals, then just unregister this listener
+        unregister();
+    }
+
+    private void checkTaskLaunchFailed() {
+        if (mLaunchedTaskId != INVALID_TASK_ID) {
+            final int launchedTaskId = mLaunchedTaskId;
+            final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback;
+            RecentsModel.INSTANCE.getNoCreate().isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> {
+                if (taskRemoved) {
+                    ActiveGestureLog.INSTANCE.addLog("Launch failed, task (id=" + launchedTaskId
+                            + ") finished mid transition");
+                    taskLaunchFailedCallback.run();
+                }
+            }, (task) -> true /* filter */);
+            unregister();
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 379722b..1cfaf14 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -321,7 +321,7 @@
     }
 
     @Override
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         launchTasks();
         callback.accept(true);
     }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 5bfd035..c6c84bd 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -237,9 +237,9 @@
     }
 
     @Override
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         getRecentsView().getSplitSelectController().launchTasks(mTask.key.id, mSecondaryTask.key.id,
-                SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList,
+                SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, isQuickswitch,
                 getSplitRatio());
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 8ff0e9b..2008129 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.PendingSplitSelectInfo;
@@ -79,9 +80,11 @@
     }
 
     @Override
-    public void startHome() {
-        mActivity.getStateManager().goToState(NORMAL);
-        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
+    public void startHome(boolean animated) {
+        StateManager stateManager = mActivity.getStateManager();
+        animated &= stateManager.shouldAnimateStateChange();
+        stateManager.goToState(NORMAL, animated);
+        AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index f0afa69..ff5af28 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2285,7 +2285,11 @@
         }
     }
 
-    public abstract void startHome();
+    public void startHome() {
+        startHome(mActivity.isStarted());
+    }
+
+    public abstract void startHome(boolean animated);
 
     public void reset() {
         setCurrentTask(-1);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 53660b5..796cd62 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -102,6 +102,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.util.TaskRemovedDuringLaunchListener;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -815,23 +816,44 @@
      * Starts the task associated with this view without any animation
      */
     public void launchTask(@NonNull Consumer<Boolean> callback) {
-        launchTask(callback, false /* freezeTaskList */);
+        launchTask(callback, false /* isQuickswitch */);
     }
 
     /**
      * Starts the task associated with this view without any animation
      */
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         if (mTask != null) {
             TestLogging.recordEvent(
                     TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
 
+            final TaskRemovedDuringLaunchListener
+                    failureListener = new TaskRemovedDuringLaunchListener();
+            if (isQuickswitch) {
+                // We only listen for failures to launch in quickswitch because the during this
+                // gesture launcher is in the background state, vs other launches which are in
+                // the actual overview state
+                failureListener.register(mActivity, mTask.key.id, () -> {
+                    notifyTaskLaunchFailed(TAG);
+                    // Disable animations for now, as it is an edge case and the app usually covers
+                    // launcher and also any state transition animation also gets clobbered by
+                    // QuickstepTransitionManager.createWallpaperOpenAnimations when launcher
+                    // shows again
+                    getRecentsView().startHome(false /* animated */);
+                });
+            }
             // Indicate success once the system has indicated that the transition has started
-            ActivityOptions opts = makeCustomAnimation(getContext(), 0, 0,
-                    () -> callback.accept(true), MAIN_EXECUTOR.getHandler());
+            ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(getContext(), 0, 0,
+                    MAIN_EXECUTOR.getHandler(),
+                    elapsedRealTime -> {
+                        callback.accept(true);
+                    },
+                    elapsedRealTime -> {
+                        failureListener.onTransitionFinished();
+                    });
             opts.setLaunchDisplayId(
                     getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
-            if (freezeTaskList) {
+            if (isQuickswitch) {
                 opts.setFreezeRecentTasksReordering();
             }
             opts.setDisableStartingWindow(mSnapshotView.shouldShowSplashView());