Use already-running app instances when splitting

This patch makes it so that when the user initiates a split from Home, a running app instance is used instead of always attempting to spawn a new Intent. If no matching app instance is found, behavior is unchanged.

Previously, splitting from Home always resulted in a new Intent being staged, which ignored the fact that an app of the same type could already be running. This resulted in undesirable situations like being able to attempt splitting an app with itself in Overview.

Fixed by querying the RecentsModel when a split of this type is initiated, checking to see if there is a running task of the desired type, and using that to perform the split operation if one is found. When Overview is loaded, applyLoadPlan() will now check to see if there is a staged task, and remove the associated tile as needed. If the removed task is part of a pair, this involves creating a temporary TaskView to hold the other task.

Also fixes a bug where using the Taskbar to select one's second app would incorrectly choose the oldest instance of a multi-instance app rather than the newest.

Fixes: 257513447
Fixes: 259936298
Test: Manual
Change-Id: I97a62f34c03aa4980f9cd743e35e9fc6ef4c092d
diff --git a/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java b/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java
index c65fa5f..9554bd3 100644
--- a/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java
+++ b/quickstep/src/com/android/launcher3/popup/QuickstepSystemShortcut.java
@@ -30,6 +30,8 @@
 import android.util.Log;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
@@ -41,6 +43,9 @@
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.RecentsView;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.function.Consumer;
 
 public interface QuickstepSystemShortcut {
 
@@ -93,14 +98,22 @@
             }
 
             StatsLogManager.EventEnum splitEvent = getLogEventForPosition(mPosition.stagePosition);
-            SplitSelectSource source = new SplitSelectSource(mOriginalView,
-                    new BitmapDrawable(bitmap), intent, mPosition, mItemInfo, splitEvent);
-            if (ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
-                startSplitToHome(source);
-            } else {
-                RecentsView recentsView = mTarget.getOverviewPanel();
-                recentsView.initiateSplitSelect(source);
-            }
+            RecentsView recentsView = mTarget.getOverviewPanel();
+            // Check if there is already an instance of this app running, if so, initiate the split
+            // using that.
+            recentsView.findLastActiveTaskAndDoSplitOperation(
+                    intent.getComponent(),
+                    (Consumer<Task>) foundTask -> {
+                        SplitSelectSource source = new SplitSelectSource(mOriginalView,
+                                new BitmapDrawable(bitmap), intent, mPosition, mItemInfo,
+                                splitEvent, foundTask);
+                        if (ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE.get()) {
+                            startSplitToHome(source);
+                        } else {
+                            recentsView.initiateSplitSelect(source);
+                        }
+                    }
+            );
         }
 
         private void startSplitToHome(SplitSelectSource source) {
@@ -108,7 +121,7 @@
 
             SplitSelectStateController controller = mTarget.getSplitSelectStateController();
             controller.setInitialTaskSelect(source.intent, source.position.stagePosition,
-                    source.itemInfo, source.splitEvent);
+                    source.itemInfo, source.splitEvent, source.alreadyRunningTask);
 
             RecentsView recentsView = mTarget.getOverviewPanel();
             recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
@@ -142,16 +155,19 @@
         public final SplitPositionOption position;
         public final ItemInfo itemInfo;
         public final StatsLogManager.EventEnum splitEvent;
+        @Nullable
+        public final Task alreadyRunningTask;
 
         public SplitSelectSource(View view, Drawable drawable, Intent intent,
                 SplitPositionOption position, ItemInfo itemInfo,
-                StatsLogManager.EventEnum splitEvent) {
+                StatsLogManager.EventEnum splitEvent, @Nullable Task foundTask) {
             this.view = view;
             this.drawable = drawable;
             this.intent = intent;
             this.position = position;
             this.itemInfo = itemInfo;
             this.splitEvent = splitEvent;
+            this.alreadyRunningTask = foundTask;
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index a059295..379d538 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -27,8 +27,10 @@
 import com.android.launcher3.model.data.ItemInfoWithIcon;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
+import com.android.systemui.shared.recents.model.Task;
 
 import java.io.PrintWriter;
+import java.util.function.Consumer;
 
 /**
  * Base class for providing different taskbar UI
@@ -157,23 +159,30 @@
      */
     public void triggerSecondAppForSplit(ItemInfoWithIcon info, Intent intent, View startingView) {
         RecentsView recents = getRecentsView();
-        TaskView foundTaskView = recents.getTaskViewByComponentName(info.getTargetComponent());
-        if (foundTaskView != null) {
-            recents.confirmSplitSelect(
-                    foundTaskView,
-                    foundTaskView.getTask(),
-                    foundTaskView.getIconView().getDrawable(),
-                    foundTaskView.getThumbnail(),
-                    foundTaskView.getThumbnail().getThumbnail(),
-                    /* intent */ null);
-        } else {
-            recents.confirmSplitSelect(
-                    /* containerTaskView */ null,
-                    /* task */ null,
-                    new BitmapDrawable(info.bitmap.icon),
-                    startingView,
-                    /* thumbnail */ null,
-                    intent);
-        }
+        recents.findLastActiveTaskAndDoSplitOperation(
+                info.getTargetComponent(),
+                (Consumer<Task>) foundTask -> {
+                    if (foundTask != null) {
+                        TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
+                        // There is already a running app of this type, use that as second app.
+                        recents.confirmSplitSelect(
+                                foundTaskView,
+                                foundTaskView.getTask(),
+                                foundTaskView.getIconView().getDrawable(),
+                                foundTaskView.getThumbnail(),
+                                foundTaskView.getThumbnail().getThumbnail(),
+                                null /* intent */);
+                    } else {
+                        // No running app of that type, create a new instance as second app.
+                        recents.confirmSplitSelect(
+                                null /* containerTaskView */,
+                                null /* task */,
+                                new BitmapDrawable(info.bitmap.icon),
+                                startingView,
+                                null /* thumbnail */,
+                                intent);
+                    }
+                }
+        );
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 3053474..3d6da8e 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -96,7 +96,8 @@
     }
 
     /**
-     * Fetches the list of recent tasks.
+     * Fetches the list of recent tasks. Tasks are ordered by recency, with the latest active tasks
+     * at the end of the list.
      *
      * @param callback The callback to receive the task plan once its complete or null. This is
      *                always called on the UI thread.
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index c263fe8..681f068 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -37,7 +37,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
 import android.view.RemoteAnimationAdapter;
@@ -124,10 +123,15 @@
      * To be called after first task selected from home or all apps.
      */
     public void setInitialTaskSelect(Intent intent, @StagePosition int stagePosition,
-            @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent) {
-        mInitialTaskIntent = intent;
-        mUser = itemInfo.user;
-        mItemInfo = itemInfo;
+            @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent,
+            @Nullable Task alreadyRunningTask) {
+        if (alreadyRunningTask != null) {
+            mInitialTaskId = alreadyRunningTask.key.id;
+        } else {
+            mInitialTaskIntent = intent;
+            mUser = itemInfo.user;
+        }
+
         setInitialData(stagePosition, splitEvent, itemInfo);
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8b406ec..c10597e 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1221,18 +1221,50 @@
     }
 
     /**
-     * Returns a {@link TaskView} that has ComponentName matching {@code componentName} or null if
-     * no match.
+     * Pulls the list of active Tasks from RecentModel, and finds the most recently active Task
+     * matching a given ComponentName. Then uses that Task (which could be null) with the given
+     * callback.
+     *
+     * Used in various splitscreen operations when we need to check if there is a currently running
+     * Task of a certain type and use the most recent one.
      */
-    @Nullable
-    public TaskView getTaskViewByComponentName(ComponentName componentName) {
-        for (int i = 0; i < getTaskViewCount(); i++) {
-            TaskView taskView = requireTaskViewAt(i);
-            if (taskView.getTask().key.sourceComponent.equals(componentName)) {
-                return taskView;
+    public void findLastActiveTaskAndDoSplitOperation(ComponentName componentName,
+            Consumer<Task> callback) {
+        mModel.getTasks(taskGroups -> {
+            Task lastActiveTask = null;
+            // Loop through tasks in reverse, since they are ordered with most-recent tasks last.
+            for (int i = taskGroups.size() - 1; i >= 0; i--) {
+                GroupTask groupTask = taskGroups.get(i);
+                Task task1 = groupTask.task1;
+                if (isInstanceOfComponent(task1, componentName)) {
+                    lastActiveTask = task1;
+                    break;
+                }
+                Task task2 = groupTask.task2;
+                if (isInstanceOfComponent(task2, componentName)) {
+                    lastActiveTask = task2;
+                    break;
+                }
             }
+
+            callback.accept(lastActiveTask);
+        });
+    }
+
+    /**
+     * Checks if a given Task is the most recently-active Task of type componentName. Used for
+     * selecting already-running Tasks for splitscreen.
+     */
+    public boolean isInstanceOfComponent(@Nullable Task task, ComponentName componentName) {
+        if (task == null) {
+            return false;
         }
-        return null;
+        // Exclude the task that is already staged
+        if (mSplitHiddenTaskView != null && mSplitHiddenTaskView.getTask().equals(task)) {
+            return false;
+        }
+
+        return task.key.baseIntent.getComponent().equals(componentName);
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
@@ -1509,13 +1541,41 @@
         int previousCurrentPage = mCurrentPage;
         removeAllViews();
 
-        // Add views as children based on whether it's grouped or single task
+        // If we are entering Overview as a result of initiating a split from somewhere else
+        // (e.g. split from Home), we need to make sure the staged app is not drawn as a thumbnail.
+        Task stagedTaskToBeRemovedFromGrid =
+                mSplitSelectSource != null ? mSplitSelectSource.alreadyRunningTask : null;
+
+        // Add views as children based on whether it's grouped or single task. Looping through
+        // taskGroups backwards populates the thumbnail grid from least recent to most recent.
         for (int i = taskGroups.size() - 1; i >= 0; i--) {
             GroupTask groupTask = taskGroups.get(i);
-            TaskView taskView = getTaskViewFromPool(groupTask.taskViewType);
+            boolean isRemovalNeeded = stagedTaskToBeRemovedFromGrid != null
+                    && groupTask.containsTask(stagedTaskToBeRemovedFromGrid.key.id);
+
+            TaskView taskView;
+            if (isRemovalNeeded && groupTask.hasMultipleTasks()) {
+                // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
+                // to be a temporary container for the remaining task.
+                taskView = getTaskViewFromPool(TaskView.Type.SINGLE);
+            } else {
+                taskView = getTaskViewFromPool(groupTask.taskViewType);
+            }
+
             addView(taskView);
 
-            if (taskView instanceof GroupedTaskView) {
+            if (isRemovalNeeded && groupTask.hasMultipleTasks()) {
+                if (groupTask.task1.equals(stagedTaskToBeRemovedFromGrid)) {
+                    taskView.bind(groupTask.task2, mOrientationState);
+                } else {
+                    taskView.bind(groupTask.task1, mOrientationState);
+                }
+            } else if (isRemovalNeeded) {
+                // If the task we need to remove is not part of a pair, bind it to the TaskView
+                // first (to prevent problems), then remove the whole thing.
+                taskView.bind(groupTask.task1, mOrientationState);
+                removeView(taskView);
+            } else if (taskView instanceof GroupedTaskView) {
                 boolean firstTaskIsLeftTopTask =
                         groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id;
                 Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2;
@@ -4269,7 +4329,7 @@
         mSplitSelectSource = splitSelectSource;
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
                 splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
-                splitSelectSource.splitEvent);
+                splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTask);
     }
 
     /**