Update recent tasks split pairs from split controller

- Add pairs for the top visible children when both roots are populated,
  and update when either side changes.
- Remove pairs if an activity is finished, or if the user dismisses
  split manually
- Remove pairs for associated tasks if they are made visible in fullscreen

Bug: 202740477
Test: atest WMShellUnitTests
Test: adb shell dumpsys activity service SystemUIService WMShell

Change-Id: I87c632dfd1a014d64ad8e25c9e68ea6ca230744e
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index a564949..ac2e448 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -240,8 +240,12 @@
     @WMSingleton
     @Provides
     static FullscreenTaskListener provideFullscreenTaskListener(
-            SyncTransactionQueue syncQueue, Optional<FullscreenUnfoldController> controller) {
-        return new FullscreenTaskListener(syncQueue, controller);
+            SyncTransactionQueue syncQueue,
+            Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController,
+            Optional<RecentTasksController> recentTasksOptional
+    ) {
+        return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController,
+                recentTasksOptional);
     }
 
     //
@@ -490,12 +494,13 @@
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) {
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
             return Optional.of(new SplitScreenController(shellTaskOrganizer, syncQueue, context,
                     rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController,
                     displayInsetsController, transitions, transactionPool, iconProvider,
-                    stageTaskUnfoldControllerProvider));
+                    recentTasks, stageTaskUnfoldControllerProvider));
         } else {
             return Optional.empty();
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 3f17f2b..6e38e42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -35,6 +35,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -47,15 +48,23 @@
     private static final String TAG = "FullscreenTaskListener";
 
     private final SyncTransactionQueue mSyncQueue;
+    private final FullscreenUnfoldController mFullscreenUnfoldController;
+    private final Optional<RecentTasksController> mRecentTasksOptional;
 
     private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
     private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener();
-    private final FullscreenUnfoldController mFullscreenUnfoldController;
 
     public FullscreenTaskListener(SyncTransactionQueue syncQueue,
             Optional<FullscreenUnfoldController> unfoldController) {
+        this(syncQueue, unfoldController, Optional.empty());
+    }
+
+    public FullscreenTaskListener(SyncTransactionQueue syncQueue,
+            Optional<FullscreenUnfoldController> unfoldController,
+            Optional<RecentTasksController> recentTasks) {
         mSyncQueue = syncQueue;
         mFullscreenUnfoldController = unfoldController.orElse(null);
+        mRecentTasksOptional = recentTasks;
     }
 
     @Override
@@ -79,6 +88,7 @@
         });
 
         mAnimatableTasksListener.onTaskAppeared(taskInfo);
+        updateRecentsForVisibleFullscreenTask(taskInfo);
     }
 
     @Override
@@ -86,6 +96,7 @@
         if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
 
         mAnimatableTasksListener.onTaskInfoChanged(taskInfo);
+        updateRecentsForVisibleFullscreenTask(taskInfo);
 
         final TaskData data = mDataByTaskId.get(taskInfo.taskId);
         final Point positionInParent = taskInfo.positionInParent;
@@ -111,6 +122,15 @@
                 taskInfo.taskId);
     }
 
+    private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) {
+        mRecentTasksOptional.ifPresent(recentTasks -> {
+            if (taskInfo.isVisible) {
+                // Remove any persisted splits if either tasks are now made fullscreen and visible
+                recentTasks.removeSplitPair(taskInfo.taskId);
+            }
+        });
+    }
+
     @Override
     public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) {
         if (!mDataByTaskId.contains(taskId)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 83a0e60..7457be2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -66,6 +66,7 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 
@@ -124,16 +125,31 @@
     private final TransactionPool mTransactionPool;
     private final SplitscreenEventLogger mLogger;
     private final IconProvider mIconProvider;
+    private final Optional<RecentTasksController> mRecentTasksOptional;
     private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider;
 
     private StageCoordinator mStageCoordinator;
 
+    // TODO(b/205019015): Remove after we clean up downstream modules
+    public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue, Context context,
+            RootTaskDisplayAreaOrganizer rootTDAOrganizer,
+            ShellExecutor mainExecutor, DisplayImeController displayImeController,
+            DisplayInsetsController displayInsetsController,
+            Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
+            Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
+        this(shellTaskOrganizer, syncQueue, context, rootTDAOrganizer, mainExecutor,
+                displayImeController, displayInsetsController, transitions, transactionPool,
+                iconProvider, Optional.empty(), unfoldControllerProvider);
+    }
+
     public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue, Context context,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer,
             ShellExecutor mainExecutor, DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
             Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
@@ -147,6 +163,7 @@
         mUnfoldControllerProvider = unfoldControllerProvider;
         mLogger = new SplitscreenEventLogger();
         mIconProvider = iconProvider;
+        mRecentTasksOptional = recentTasks;
     }
 
     public SplitScreen asSplitScreen() {
@@ -169,7 +186,7 @@
             mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
                     mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController,
                     mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
-                    mIconProvider, mUnfoldControllerProvider);
+                    mIconProvider, mRecentTasksOptional, mUnfoldControllerProvider);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 44af43d..3c35e6a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.splitscreen;
 
 import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_OPEN;
@@ -92,6 +93,7 @@
 import com.android.wm.shell.common.split.SplitLayout.SplitPosition;
 import com.android.wm.shell.common.split.SplitWindowManager;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
 import com.android.wm.shell.transition.Transitions;
 
@@ -147,6 +149,10 @@
     private final DisplayInsetsController mDisplayInsetsController;
     private final SplitScreenTransitions mSplitTransitions;
     private final SplitscreenEventLogger mLogger;
+    private final Optional<RecentTasksController> mRecentTasks;
+    // Tracks whether we should update the recent tasks.  Only allow this to happen in between enter
+    // and exit, since exit itself can trigger a number of changes that update the stages.
+    private boolean mShouldUpdateRecents;
     private boolean mExitSplitScreenOnHide;
     private boolean mKeyguardOccluded;
 
@@ -191,6 +197,7 @@
             DisplayInsetsController displayInsetsController, Transitions transitions,
             TransactionPool transactionPool, SplitscreenEventLogger logger,
             IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
@@ -198,6 +205,7 @@
         mRootTDAOrganizer = rootTDAOrganizer;
         mTaskOrganizer = taskOrganizer;
         mLogger = logger;
+        mRecentTasks = recentTasks;
         mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
         mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
 
@@ -238,6 +246,7 @@
             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
             Transitions transitions, TransactionPool transactionPool,
             SplitscreenEventLogger logger,
+            Optional<RecentTasksController> recentTasks,
             Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) {
         mContext = context;
         mDisplayId = displayId;
@@ -255,6 +264,7 @@
         mMainUnfoldController = unfoldControllerProvider.get().orElse(null);
         mSideUnfoldController = unfoldControllerProvider.get().orElse(null);
         mLogger = logger;
+        mRecentTasks = recentTasks;
         transitions.addHandler(this);
     }
 
@@ -560,12 +570,23 @@
 
     private void applyExitSplitScreen(StageTaskListener childrenToTop,
             WindowContainerTransaction wct, @ExitReason int exitReason) {
+        mRecentTasks.ifPresent(recentTasks -> {
+            // Notify recents if we are exiting in a way that breaks the pair, and disable further
+            // updates to splits in the recents until we enter split again
+            if (shouldBreakPairedTaskInRecents(exitReason) && mShouldUpdateRecents) {
+                recentTasks.removeSplitPair(mMainStage.getTopVisibleChildTaskId());
+                recentTasks.removeSplitPair(mSideStage.getTopVisibleChildTaskId());
+            }
+        });
+        mShouldUpdateRecents = false;
+
         mSideStage.removeAllTasks(wct, childrenToTop == mSideStage);
         mMainStage.deactivate(wct, childrenToTop == mMainStage);
         mTaskOrganizer.applyTransaction(wct);
         mSyncQueue.runInSync(t -> t
                 .setWindowCrop(mMainStage.mRootLeash, null)
                 .setWindowCrop(mSideStage.mRootLeash, null));
+
         // Hide divider and reset its position.
         setDividerVisibility(false);
         mSplitLayout.resetDividerPosition();
@@ -580,6 +601,23 @@
     }
 
     /**
+     * Returns whether the split pair in the recent tasks list should be broken.
+     */
+    private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) {
+        switch (exitReason) {
+            // One of the apps doesn't support MW
+            case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
+            // User has explicitly dragged the divider to dismiss split
+            case EXIT_REASON_DRAG_DIVIDER:
+            // Either of the split apps have finished
+            case EXIT_REASON_APP_FINISHED:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
      * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
      * an existing WindowContainerTransaction (rather than applying immediately). This is intended
      * to be used when exiting split might be bundled with other window operations.
@@ -645,12 +683,28 @@
             mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
                     mSplitLayout.isLandscape());
         }
+        updateRecentTasksSplitPair();
 
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
         }
     }
 
+    private void updateRecentTasksSplitPair() {
+        if (!mShouldUpdateRecents) {
+            return;
+        }
+
+        mRecentTasks.ifPresent(recentTasks -> {
+            int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId();
+            int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId();
+            if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) {
+                // Update the pair for the top tasks
+                recentTasks.addSplitPair(mainStageTopTaskId, sideStageTopTaskId);
+            }
+        });
+    }
+
     private void sendSplitVisibilityChanged() {
         for (int i = mListeners.size() - 1; i >= 0; --i) {
             final SplitScreen.SplitScreenListener l = mListeners.get(i);
@@ -784,12 +838,16 @@
             mSyncQueue.queue(wct);
             mSyncQueue.runInSync(t -> updateSurfaceBounds(mSplitLayout, t));
         }
-        if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren
-                && mSideStageListener.mHasChildren) {
-            mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
-                    getMainStagePosition(), mMainStage.getTopChildTaskUid(),
-                    getSideStagePosition(), mSideStage.getTopChildTaskUid(),
-                    mSplitLayout.isLandscape());
+        if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
+            mShouldUpdateRecents = true;
+            updateRecentTasksSplitPair();
+
+            if (!mLogger.hasStartedSession()) {
+                mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
+                        getMainStagePosition(), mMainStage.getTopChildTaskUid(),
+                        getSideStagePosition(), mSideStage.getTopChildTaskUid(),
+                        mSplitLayout.isLandscape());
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 5100c56..190006e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.splitscreen;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
@@ -113,6 +114,19 @@
     }
 
     /**
+     * Returns the top visible child task's id.
+     */
+    int getTopVisibleChildTaskId() {
+        for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) {
+            final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i);
+            if (info.isVisible) {
+                return info.taskId;
+            }
+        }
+        return INVALID_TASK_ID;
+    }
+
+    /**
      * Returns the top activity uid for the top child task.
      */
     int getTopChildTaskUid() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
index d6f7e54..9cbdf1e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java
@@ -35,6 +35,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.recents.RecentTasksController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -52,6 +53,8 @@
     @Mock
     private FullscreenUnfoldController mUnfoldController;
     @Mock
+    private RecentTasksController mRecentTasksController;
+    @Mock
     private SurfaceControl mSurfaceControl;
 
     private Optional<FullscreenUnfoldController> mFullscreenUnfoldController;
@@ -62,7 +65,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mFullscreenUnfoldController = Optional.of(mUnfoldController);
-        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController);
+        mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController,
+                Optional.empty());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index f90af23..aab1e3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.Optional;
@@ -72,10 +73,11 @@
                 DisplayInsetsController insetsController, SplitLayout splitLayout,
                 Transitions transitions, TransactionPool transactionPool,
                 SplitscreenEventLogger logger,
+                Optional<RecentTasksController> recentTasks,
                 Provider<Optional<StageTaskUnfoldController>> unfoldController) {
             super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage,
                     sideStage, imeController, insetsController, splitLayout, transitions,
-                    transactionPool, logger, unfoldController);
+                    transactionPool, logger, recentTasks, unfoldController);
 
             // Prepare default TaskDisplayArea for testing.
             mDisplayAreaInfo = new DisplayAreaInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index d5dee82..1eae625 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -66,6 +66,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -120,8 +121,7 @@
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
                 mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions,
-                mTransactionPool,
-                mLogger, Optional::empty);
+                mTransactionPool, mLogger, Optional.empty(), Optional::empty);
         mSplitScreenTransitions = mStageCoordinator.getSplitTransitions();
         doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class))
                 .when(mTransitions).startTransition(anyInt(), any(), any());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index 58088d8..ad65c04 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -49,6 +49,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.split.SplitLayout;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -198,7 +199,8 @@
         return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
                 mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage,
                 mDisplayImeController, mDisplayInsetsController, splitLayout,
-                mTransitions, mTransactionPool, mLogger, new UnfoldControllerProvider());
+                mTransitions, mTransactionPool, mLogger, Optional.empty(),
+                new UnfoldControllerProvider());
     }
 
     private class UnfoldControllerProvider implements