Merge "Fix grid layout problem after going back from split select" into sc-v2-dev
diff --git a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
index 9d70cfa..e1d89a1 100644
--- a/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
+++ b/quickstep/src/com/android/launcher3/statehandlers/DepthController.java
@@ -44,7 +44,6 @@
 import com.android.systemui.shared.system.BlurUtils;
 import com.android.systemui.shared.system.WallpaperManagerCompat;
 
-import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.function.Consumer;
 
@@ -156,6 +155,10 @@
     // Workaround for animating the depth when multiwindow mode changes.
     private boolean mIgnoreStateChangesDuringMultiWindowAnimation = false;
 
+    // Hints that there is potentially content behind Launcher and that we shouldn't optimize by
+    // marking the launcher surface as opaque.  Only used in certain Launcher states.
+    private boolean mHasContentBehindLauncher;
+
     private View.OnAttachStateChangeListener mOnAttachListener;
 
     public DepthController(Launcher l) {
@@ -199,6 +202,10 @@
         mLauncher.getScrimView().addOpaquenessListener(mOpaquenessListener);
     }
 
+    public void setHasContentBehindLauncher(boolean hasContentBehindLauncher) {
+        mHasContentBehindLauncher = hasContentBehindLauncher;
+    }
+
     /**
      * Sets if the underlying activity is started or not
      */
@@ -311,13 +318,14 @@
         }
 
         if (supportsBlur) {
-            boolean opaque = mLauncher.getScrimView().isFullyOpaque();
+            boolean hasOpaqueBg = mLauncher.getScrimView().isFullyOpaque();
+            boolean isSurfaceOpaque = !mHasContentBehindLauncher && hasOpaqueBg;
 
-            mCurrentBlur = !mCrossWindowBlursEnabled || mBlurDisabledForAppLaunch
+            mCurrentBlur = !mCrossWindowBlursEnabled || mBlurDisabledForAppLaunch || hasOpaqueBg
                     ? 0 : (int) (depth * mMaxBlurRadius);
             SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()
                     .setBackgroundBlurRadius(mSurface, mCurrentBlur)
-                    .setOpaque(mSurface, opaque);
+                    .setOpaque(mSurface, isSurfaceOpaque);
 
             // Set early wake-up flags when we know we're executing an expensive operation, this way
             // SurfaceFlinger will adjust its internal offsets to avoid jank.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 5354232..2e1e5bb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -20,7 +20,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
-import static com.android.launcher3.testing.TestProtocol.TASKBAR_WINDOW_CRASH;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
@@ -204,7 +203,6 @@
         updateSysuiStateFlags(sharedState.sysuiStateFlags, true /* fromInit */);
 
         mWindowManager.addView(mDragLayer, mWindowLayoutParams);
-        Log.d(TASKBAR_WINDOW_CRASH, "Adding taskbar window");
     }
 
     public boolean isThreeButtonNav() {
@@ -340,7 +338,6 @@
         setUIController(TaskbarUIController.DEFAULT);
         mControllers.onDestroy();
         mWindowManager.removeViewImmediate(mDragLayer);
-        Log.d(TASKBAR_WINDOW_CRASH, "Removing taskbar window");
     }
 
     public void updateSysuiStateFlags(int systemUiStateFlags, boolean fromInit) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 6b7c597..a65cc56 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -18,7 +18,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
-import static com.android.launcher3.testing.TestProtocol.TASKBAR_WINDOW_CRASH;
 import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
@@ -206,8 +205,6 @@
     }
 
     private void recreateTaskbar() {
-        Log.d(TASKBAR_WINDOW_CRASH, "Recreating taskbar: mTaskbarActivityContext="
-                + mTaskbarActivityContext);
         destroyExistingTaskbar();
 
         DeviceProfile dp =
@@ -230,7 +227,6 @@
             mTaskbarActivityContext.setUIController(
                     createTaskbarUIControllerForActivity(mActivity));
         }
-        Log.d(TASKBAR_WINDOW_CRASH, "Finished recreating taskbar");
     }
 
     public void onSystemUiFlagsChanged(int systemUiStateFlags) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index d21c110..b21d677 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -63,6 +63,9 @@
         }
         setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, new StateAnimationConfig(), state);
         mRecentsView.setFullscreenProgress(state.getOverviewFullscreenProgress());
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        mLauncher.getDepthController().setHasContentBehindLauncher(state.overviewUi);
     }
 
     @Override
@@ -78,6 +81,10 @@
             builder.addListener(
                     AnimatorListeners.forSuccessCallback(mRecentsView::resetTaskVisuals));
         }
+        // In Overview, we may be layering app surfaces behind Launcher, so we need to notify
+        // DepthController to prevent optimizations which might occlude the layers behind
+        builder.addListener(AnimatorListeners.forSuccessCallback(() ->
+                mLauncher.getDepthController().setHasContentBehindLauncher(toState.overviewUi)));
 
         // Create or dismiss split screen select animations
         LauncherState currentState = mLauncher.getStateManager().getState();
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index c5f4a53..097850f 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.StagedSplitBounds;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.function.Consumer;
@@ -219,6 +220,26 @@
         return newTasks;
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "RecentTasksList:");
+        writer.println(prefix + "  mChangeId=" + mChangeId);
+        writer.println(prefix + "  mResultsUi=[id=" + mResultsUi.mRequestId + ", tasks=");
+        for (GroupTask task : mResultsUi) {
+            writer.println(prefix + "    t1=" + task.task1.key.id
+                    + " t2=" + (task.hasMultipleTasks() ? task.task2.key.id : "-1"));
+        }
+        writer.println(prefix + "  ]");
+        int currentUserId = Process.myUserHandle().getIdentifier();
+        ArrayList<GroupedRecentTaskInfo> rawTasks =
+                mSysUiProxy.getRecentTasks(Integer.MAX_VALUE, currentUserId);
+        writer.println(prefix + "  rawTasks=[");
+        for (GroupedRecentTaskInfo task : rawTasks) {
+            writer.println(prefix + "    t1=" + task.mTaskInfo1.taskId
+                    + " t2=" + (task.mTaskInfo2 != null ? task.mTaskInfo2.taskId : "-1"));
+        }
+        writer.println(prefix + "  ]");
+    }
+
     private static class TaskLoadResult extends ArrayList<GroupTask> {
 
         final int mRequestId;
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index e539a8c..5d77a6e 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -43,6 +43,7 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
@@ -220,6 +221,11 @@
         mThumbnailChangeListeners.remove(listener);
     }
 
+    public void dump(String prefix, PrintWriter writer) {
+        writer.println(prefix + "RecentsModel:");
+        mTaskList.dump("  ", writer);
+    }
+
     /**
      * Listener for receiving various task properties changes
      */
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 68b7558..c8abd14 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -83,14 +83,16 @@
         MAIN_EXECUTOR.execute(() -> clearProxy());
     };
 
-    // Save the listeners passed into the proxy since when set/register these listeners,
-    // setProxy may not have been called, eg. OverviewProxyService is not connected yet.
-    private IPipAnimationListener mPendingPipAnimationListener;
-    private ISplitScreenListener mPendingSplitScreenListener;
-    private IStartingWindowListener mPendingStartingWindowListener;
-    private ISmartspaceCallback mPendingSmartspaceCallback;
-    private IRecentTasksListener mPendingRecentTasksListener;
-    private final ArrayList<RemoteTransitionCompat> mPendingRemoteTransitions = new ArrayList<>();
+    // Save the listeners passed into the proxy since OverviewProxyService may not have been bound
+    // yet, and we'll need to set/register these listeners with SysUI when they do.  Note that it is
+    // up to the caller to clear the listeners to prevent leaks as these can be held indefinitely
+    // in case SysUI needs to rebind.
+    private IPipAnimationListener mPipAnimationListener;
+    private ISplitScreenListener mSplitScreenListener;
+    private IStartingWindowListener mStartingWindowListener;
+    private ISmartspaceCallback mSmartspaceCallback;
+    private IRecentTasksListener mRecentTasksListener;
+    private final ArrayList<RemoteTransitionCompat> mRemoteTransitions = new ArrayList<>();
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -167,29 +169,23 @@
         mRecentTasks = recentTasks;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
-        if (mPendingPipAnimationListener != null && mPip != null) {
-            setPinnedStackAnimationListener(mPendingPipAnimationListener);
-            mPendingPipAnimationListener = null;
+        if (mPipAnimationListener != null && mPip != null) {
+            setPinnedStackAnimationListener(mPipAnimationListener);
         }
-        if (mPendingSplitScreenListener != null && mSplitScreen != null) {
-            registerSplitScreenListener(mPendingSplitScreenListener);
-            mPendingSplitScreenListener = null;
+        if (mSplitScreenListener != null && mSplitScreen != null) {
+            registerSplitScreenListener(mSplitScreenListener);
         }
-        if (mPendingStartingWindowListener != null && mStartingWindow != null) {
-            setStartingWindowListener(mPendingStartingWindowListener);
-            mPendingStartingWindowListener = null;
+        if (mStartingWindowListener != null && mStartingWindow != null) {
+            setStartingWindowListener(mStartingWindowListener);
         }
-        if (mPendingSmartspaceCallback != null && mSmartspaceTransitionController != null) {
-            setSmartspaceCallback(mPendingSmartspaceCallback);
-            mPendingSmartspaceCallback = null;
+        if (mSmartspaceCallback != null && mSmartspaceTransitionController != null) {
+            setSmartspaceCallback(mSmartspaceCallback);
         }
-        for (int i = mPendingRemoteTransitions.size() - 1; i >= 0; --i) {
-            registerRemoteTransition(mPendingRemoteTransitions.get(i));
+        for (int i = mRemoteTransitions.size() - 1; i >= 0; --i) {
+            registerRemoteTransition(mRemoteTransitions.get(i));
         }
-        mPendingRemoteTransitions.clear();
-        if (mPendingRecentTasksListener != null && mRecentTasks != null) {
-            registerRecentTasksListener(mPendingRecentTasksListener);
-            mPendingRecentTasksListener = null;
+        if (mRecentTasksListener != null && mRecentTasks != null) {
+            registerRecentTasksListener(mRecentTasksListener);
         }
 
         if (mPendingSetNavButtonAlpha != null) {
@@ -513,9 +509,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setPinnedStackAnimationListener", e);
             }
-        } else {
-            mPendingPipAnimationListener = listener;
         }
+        mPipAnimationListener = listener;
     }
 
     public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo,
@@ -553,9 +548,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerSplitScreenListener");
             }
-        } else {
-            mPendingSplitScreenListener = listener;
         }
+        mSplitScreenListener = listener;
     }
 
     public void unregisterSplitScreenListener(ISplitScreenListener listener) {
@@ -566,7 +560,7 @@
                 Log.w(TAG, "Failed call unregisterSplitScreenListener");
             }
         }
-        mPendingSplitScreenListener = null;
+        mSplitScreenListener = null;
     }
 
     /** Start multiple tasks in split-screen simultaneously. */
@@ -687,9 +681,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
-        } else {
-            mPendingRemoteTransitions.add(remoteTransition);
         }
+        mRemoteTransitions.add(remoteTransition);
     }
 
     public void unregisterRemoteTransition(RemoteTransitionCompat remoteTransition) {
@@ -700,7 +693,7 @@
                 Log.w(TAG, "Failed call registerRemoteTransition");
             }
         }
-        mPendingRemoteTransitions.remove(remoteTransition);
+        mRemoteTransitions.remove(remoteTransition);
     }
 
     //
@@ -717,9 +710,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
-        } else {
-            mPendingStartingWindowListener = listener;
         }
+        mStartingWindowListener = listener;
     }
 
     //
@@ -733,9 +725,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call setStartingWindowListener", e);
             }
-        } else {
-            mPendingSmartspaceCallback = callback;
         }
+        mSmartspaceCallback = callback;
     }
 
     //
@@ -749,9 +740,8 @@
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call registerRecentTasksListener", e);
             }
-        } else {
-            mPendingRecentTasksListener = listener;
         }
+        mRecentTasksListener = listener;
     }
 
     public void unregisterRecentTasksListener(IRecentTasksListener listener) {
@@ -762,7 +752,7 @@
                 Log.w(TAG, "Failed call unregisterRecentTasksListener");
             }
         }
-        mPendingRecentTasksListener = null;
+        mRecentTasksListener = null;
     }
 
     public ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 8c4ba97..cbdbdb5 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -72,7 +72,13 @@
         @Override
         public SystemShortcut getShortcut(BaseDraggingActivity activity,
                 TaskIdAttributeContainer taskContainer) {
-            return new AppInfo(activity, taskContainer.getItemInfo());
+            TaskView taskView = taskContainer.getTaskView();
+            AppInfo.SplitAccessibilityInfo accessibilityInfo =
+                    new AppInfo.SplitAccessibilityInfo(taskView.containsMultipleTasks(),
+                            TaskUtils.getTitle(taskView.getContext(), taskContainer.getTask()),
+                            taskContainer.getA11yNodeId()
+                    );
+            return new AppInfo(activity, taskContainer.getItemInfo(), accessibilityInfo);
         }
 
         @Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index bda5a30..f6f2cf9 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -22,7 +22,6 @@
 
 import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.testing.TestProtocol.TASKBAR_WINDOW_CRASH;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
@@ -361,7 +360,6 @@
     @Override
     public void onCreate() {
         super.onCreate();
-        Log.d(TASKBAR_WINDOW_CRASH, "TIS created");
         // Initialize anything here that is needed in direct boot mode.
         // Everything else should be initialized in onUserUnlocked() below.
         mMainChoreographer = Choreographer.getInstance();
@@ -523,7 +521,6 @@
     @Override
     public void onDestroy() {
         Log.d(TAG, "Touch service destroyed: user=" + getUserId());
-        Log.d(TASKBAR_WINDOW_CRASH, "TIS destroyed");
         sIsInitialized = false;
         if (mDeviceState.isUserUnlocked()) {
             mInputConsumer.unregisterInputConsumer();
@@ -976,6 +973,7 @@
             pw.println("  resumed=" + resumed);
             pw.println("  mConsumer=" + mConsumer.getName());
             ActiveGestureLog.INSTANCE.dump("", pw);
+            RecentsModel.INSTANCE.get(this).dump("", pw);
             pw.println("ProtoTrace:");
             pw.println("  file=" + ProtoTracer.INSTANCE.get(this).getTraceFile());
         }
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 95095fa..22f67d2 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -16,7 +16,6 @@
 package com.android.quickstep.fallback;
 
 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
-import static com.android.quickstep.ViewUtils.postFrameDrawn;
 import static com.android.quickstep.fallback.RecentsState.DEFAULT;
 import static com.android.quickstep.fallback.RecentsState.HOME;
 import static com.android.quickstep.fallback.RecentsState.MODAL_TASK;
@@ -224,8 +223,8 @@
         setFreezeViewVisibility(false);
 
         if (isOverlayEnabled) {
-            postFrameDrawn(this, () -> runActionOnRemoteHandles(remoteTargetHandle ->
-                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)));
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
index ccb1a99..5895c05 100644
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ b/quickstep/src/com/android/quickstep/views/IconView.java
@@ -87,6 +87,14 @@
         return mDrawable;
     }
 
+    public int getDrawableWidth() {
+        return mDrawableWidth;
+    }
+
+    public int getDrawableHeight() {
+        return mDrawableHeight;
+    }
+
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 3cba392..a2e9e57 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.OVERVIEW_SPLIT_SELECT;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
-import static com.android.quickstep.ViewUtils.postFrameDrawn;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -110,8 +109,8 @@
         setFreezeViewVisibility(false);
 
         if (isOverlayEnabled) {
-            postFrameDrawn(this, () -> runActionOnRemoteHandles(remoteTargetHandle ->
-                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true)));
+            runActionOnRemoteHandles(remoteTargetHandle ->
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(true));
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 1f14146..3aa8d46 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2612,10 +2612,8 @@
                         clampToProgress(FINAL_FRAME, 0, 0.5f));
             });
         }
-        boolean isTaskInBottomGridRow = showAsGrid() && !mTopRowIdSet.contains(
-                taskView.getTaskViewId()) && taskView.getTaskViewId() != mFocusedTaskViewId;
         anim.setFloat(taskView, VIEW_ALPHA, 0,
-                clampToProgress(isTaskInBottomGridRow ? ACCEL : FINAL_FRAME, 0, 0.5f));
+                clampToProgress(isOnGridBottomRow(taskView) ? ACCEL : FINAL_FRAME, 0, 0.5f));
         FloatProperty<TaskView> secondaryViewTranslate =
                 taskView.getSecondaryDissmissTranslationProperty();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
@@ -4676,6 +4674,15 @@
         return position != -1 ? position : bottomRowIdArray.indexOf(taskView.getTaskViewId());
     }
 
+    /**
+     * @return true if the task in on the top of the grid
+     */
+    public boolean isOnGridBottomRow(TaskView taskView) {
+        return showAsGrid()
+                && !mTopRowIdSet.contains(taskView.getTaskViewId())
+                && taskView.getTaskViewId() != mFocusedTaskViewId;
+    }
+
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
index cd1691b..06a5793 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuViewWithArrow.kt
@@ -45,7 +45,10 @@
     companion object {
         const val TAG = "TaskMenuViewWithArrow"
 
-        fun showForTask(taskContainer: TaskIdAttributeContainer): Boolean {
+        fun showForTask(
+            taskContainer: TaskIdAttributeContainer,
+            alignSecondRow: Boolean = false
+        ): Boolean {
             val activity = BaseDraggingActivity
                 .fromContext<BaseDraggingActivity>(taskContainer.taskView.context)
             val taskMenuViewWithArrow = activity.layoutInflater
@@ -55,7 +58,7 @@
                     false
                 ) as TaskMenuViewWithArrow<*>
 
-            return taskMenuViewWithArrow.populateAndShowForTask(taskContainer)
+            return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignSecondRow)
         }
     }
 
@@ -78,6 +81,9 @@
         CLOSE_FADE_DURATION = CLOSE_CHILD_FADE_DURATION
     }
 
+    private var alignSecondRow: Boolean = false
+    private val extraSpaceForSecondRowAlignment: Int
+        get() = if (alignSecondRow) optionMeasuredHeight else 0
     private val menuWidth = context.resources.getDimensionPixelSize(R.dimen.task_menu_width_grid)
 
     private lateinit var taskView: TaskView
@@ -91,6 +97,10 @@
         else
             0
 
+    private var iconView: IconView? = null
+    private var scrim: View? = null
+    private val scrimAlpha = 0.8f
+
     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
 
     override fun getTargetObjectLocation(outPos: Rect?) {
@@ -112,18 +122,35 @@
         optionLayout = findViewById(KtR.id.menu_option_layout)
     }
 
-    private fun populateAndShowForTask(taskContainer: TaskIdAttributeContainer): Boolean {
+    private fun populateAndShowForTask(
+        taskContainer: TaskIdAttributeContainer,
+        alignSecondRow: Boolean
+    ): Boolean {
         if (isAttachedToWindow) {
             return false
         }
 
         taskView = taskContainer.taskView
         this.taskContainer = taskContainer
+        this.alignSecondRow = alignSecondRow
         if (!populateMenu()) return false
+        addScrim()
         show()
         return true
     }
 
+    private fun addScrim() {
+        scrim = View(context).apply {
+            layoutParams = FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.MATCH_PARENT,
+                FrameLayout.LayoutParams.MATCH_PARENT
+            )
+            setBackgroundColor(Themes.getAttrColor(context, R.attr.overviewScrimColor))
+            alpha = 0f
+        }
+        popupContainer.addView(scrim)
+    }
+
     /** @return true if successfully able to populate task view menu, false otherwise
      */
     private fun populateMenu(): Boolean {
@@ -180,18 +207,50 @@
     }
 
     override fun onCreateOpenAnimation(anim: AnimatorSet) {
-        anim.play(
-            ObjectAnimator.ofFloat(
-                taskContainer.thumbnailView, TaskThumbnailView.DIM_ALPHA,
-                TaskView.MAX_PAGE_SCRIM_ALPHA
+        scrim?.let {
+            anim.play(
+                ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha)
+                    .setDuration(OPEN_DURATION.toLong())
             )
-        )
+        }
     }
 
     override fun onCreateCloseAnimation(anim: AnimatorSet) {
-        anim.play(
-            ObjectAnimator.ofFloat(taskContainer.thumbnailView, TaskThumbnailView.DIM_ALPHA, 0f)
-        )
+        scrim?.let {
+            anim.play(
+                ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f)
+                    .setDuration(CLOSE_DURATION.toLong())
+            )
+        }
+    }
+
+    override fun closeComplete() {
+        super.closeComplete()
+        popupContainer.removeView(scrim)
+        popupContainer.removeView(iconView)
+    }
+
+    /**
+     * Copy the iconView from taskView to dragLayer so it can stay on top of the scrim.
+     * It needs to be called after [getTargetObjectLocation] because [mTempRect] needs to be
+     * populated.
+     */
+    private fun copyIconToDragLayer(insets: Rect) {
+        iconView = IconView(context).apply {
+            layoutParams = FrameLayout.LayoutParams(
+                taskContainer.iconView.width,
+                taskContainer.iconView.height
+            )
+            x = mTempRect.left.toFloat() - insets.left
+            y = mTempRect.top.toFloat() - insets.top
+            drawable = taskContainer.iconView.drawable
+            setDrawableSize(
+                taskContainer.iconView.drawableWidth,
+                taskContainer.iconView.drawableHeight
+            )
+        }
+
+        popupContainer.addView(iconView)
     }
 
     /**
@@ -217,7 +276,10 @@
         val dragLayer: InsettableFrameLayout = popupContainer
         val insets = dragLayer.insets
 
-        // Put to the right of the icon if there is space, which means left aligned with the menu
+        copyIconToDragLayer(insets)
+
+        // Put this menu to the right of the icon if there is space,
+        // which means the arrow is left aligned with the menu
         val rightAlignedMenuStartX = mTempRect.left - widthWithArrow
         val leftAlignedMenuStartX = mTempRect.right + extraHorizontalSpace
         mIsLeftAligned = if (mIsRtl) {
@@ -229,18 +291,17 @@
 
         var menuStartX = if (mIsLeftAligned) leftAlignedMenuStartX else rightAlignedMenuStartX
 
-        // Offset y so that the arrow and first row are center-aligned with the original icon.
+        // Offset y so that the arrow and row are center-aligned with the original icon.
         val iconHeight = mTempRect.height()
-        val optionHeight = optionMeasuredHeight
-        val yOffset = (optionHeight - iconHeight) / 2
-        var menuStartY = mTempRect.top - yOffset
+        val yOffset = (optionMeasuredHeight - iconHeight) / 2
+        var menuStartY = mTempRect.top - yOffset - extraSpaceForSecondRowAlignment
 
         // Insets are added later, so subtract them now.
         menuStartX -= insets.left
         menuStartY -= insets.top
 
-        setX(menuStartX.toFloat())
-        setY(menuStartY.toFloat())
+        x = menuStartX.toFloat()
+        y = menuStartY.toFloat()
 
         val lp = layoutParams as FrameLayout.LayoutParams
         val arrowLp = mArrow.layoutParams as FrameLayout.LayoutParams
@@ -251,7 +312,8 @@
     override fun addArrow() {
         popupContainer.addView(mArrow)
         mArrow.x = getArrowX()
-        mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2)
+        mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2) +
+                extraSpaceForSecondRowAlignment
 
         updateArrowColor()
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 1ff2a88..e8077cf 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -38,6 +39,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
+import android.annotation.IdRes;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.Intent;
@@ -840,7 +842,9 @@
         TaskIdAttributeContainer menuContainer =
                 mTaskIdAttributeContainer[iconView == mIconView ? 0 : 1];
         if (mActivity.getDeviceProfile().overviewShowAsGrid) {
-            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer);
+            boolean alignSecondRow = getRecentsView().isOnGridBottomRow(menuContainer.getTaskView())
+                    && mActivity.getDeviceProfile().isLandscape;
+            return TaskMenuViewWithArrow.Companion.showForTask(menuContainer, alignSecondRow);
         } else {
             return TaskMenuView.showForTask(menuContainer);
         }
@@ -1302,10 +1306,14 @@
                         getContext().getText(R.string.accessibility_close)));
 
         final Context context = getContext();
-        // TODO(b/200609838) Determine which task to run A11y action on when in split screen
-        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
-                mActivity.getDeviceProfile(), mTaskIdAttributeContainer[0])) {
-            info.addAction(s.createAccessibilityAction(context));
+        for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
+            if (taskContainer == null) {
+                continue;
+            }
+            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+                    mActivity.getDeviceProfile(), taskContainer)) {
+                info.addAction(s.createAccessibilityAction(context));
+            }
         }
 
         if (mDigitalWellBeingToast.hasLimit()) {
@@ -1336,12 +1344,16 @@
             return true;
         }
 
-        // TODO(b/200609838) Determine which task to run A11y action on when in split screen
-        for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
-                mActivity.getDeviceProfile(), mTaskIdAttributeContainer[0])) {
-            if (s.hasHandlerForAction(action)) {
-                s.onClick(this);
-                return true;
+        for (TaskIdAttributeContainer taskContainer : mTaskIdAttributeContainer) {
+            if (taskContainer == null) {
+                continue;
+            }
+            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+                    mActivity.getDeviceProfile(), taskContainer)) {
+                if (s.hasHandlerForAction(action)) {
+                    s.onClick(this);
+                    return true;
+                }
             }
         }
 
@@ -1558,7 +1570,6 @@
                 mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
             }
         }
-
     }
 
     public class TaskIdAttributeContainer {
@@ -1567,6 +1578,8 @@
         private final IconView mIconView;
         /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */
         private @SplitConfigurationOptions.StagePosition int mStagePosition;
+        @IdRes
+        private final int mA11yNodeId;
 
         public TaskIdAttributeContainer(Task task, TaskThumbnailView thumbnailView,
                 IconView iconView, int stagePosition) {
@@ -1574,6 +1587,8 @@
             this.mThumbnailView = thumbnailView;
             this.mIconView = iconView;
             this.mStagePosition = stagePosition;
+            this.mA11yNodeId = (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) ?
+                    R.id.split_bottomRight_appInfo : R.id.split_topLeft_appInfo;
         }
 
         public TaskThumbnailView getThumbnailView() {
@@ -1603,5 +1618,9 @@
         void setStagePosition(@SplitConfigurationOptions.StagePosition int stagePosition) {
             this.mStagePosition = stagePosition;
         }
+
+        public int getA11yNodeId() {
+            return mA11yNodeId;
+        }
     }
 }
diff --git a/res/values/id.xml b/res/values/id.xml
index ebc4075..508caff 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -21,6 +21,10 @@
     <item type="id" name="view_type_widgets_list" />
     <item type="id" name="view_type_widgets_header" />
     <item type="id" name="view_type_widgets_search_header" />
+    <!--  Used for A11y actions in staged split to identify each task uniquely  -->
+    <item type="id" name="split_topLeft_appInfo" />
+    <item type="id" name="split_bottomRight_appInfo" />
+
 
     <!--  Do not change, must be kept in sync with sysui navbar button IDs for tests!  -->
     <item type="id" name="home" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5f53d4e..868b5f3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -40,9 +40,10 @@
     <!-- Options for recent tasks -->
     <!-- Title for an option to enter split screen mode for a given app -->
     <string name="recent_task_option_split_screen">Split screen</string>
-    <string translatable="false" name="split_screen_position_top">Split top</string>
-    <string translatable="false" name="split_screen_position_left">Split left</string>
-    <string translatable="false" name="split_screen_position_right">Split right</string>
+    <string name="split_screen_position_top">Split top</string>
+    <string name="split_screen_position_left">Split left</string>
+    <string name="split_screen_position_right">Split right</string>
+    <string name="split_app_info_accessibility">App info for %1$s</string>
 
     <!-- Widgets -->
     <!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 826c79b..af87275 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -41,8 +41,8 @@
         implements View.OnClickListener {
 
     private final int mIconResId;
-    private final int mLabelResId;
-    private final int mAccessibilityActionId;
+    protected final int mLabelResId;
+    protected int mAccessibilityActionId;
 
     protected final T mTarget;
     protected final ItemInfo mItemInfo;
@@ -139,11 +139,43 @@
 
     public static class AppInfo<T extends Context & ActivityContext> extends SystemShortcut<T> {
 
+        @Nullable
+        private SplitAccessibilityInfo mSplitA11yInfo;
+
         public AppInfo(T target, ItemInfo itemInfo) {
             super(R.drawable.ic_info_no_shadow, R.string.app_info_drop_target_label, target,
                     itemInfo);
         }
 
+        /**
+         * Constructor used by overview for staged split to provide custom A11y information.
+         *
+         * Future improvements considerations:
+         * Have the logic in {@link #createAccessibilityAction(Context)} be moved to super
+         * call in {@link SystemShortcut#createAccessibilityAction(Context)} by having
+         * SystemShortcut be aware of TaskContainers and staged split.
+         * That way it could directly create the correct node info for any shortcut that supports
+         * split, but then we'll need custom resIDs for each pair of shortcuts.
+         */
+        public AppInfo(T target, ItemInfo itemInfo, SplitAccessibilityInfo accessibilityInfo) {
+            this(target, itemInfo);
+            mSplitA11yInfo = accessibilityInfo;
+            mAccessibilityActionId = accessibilityInfo.nodeId;
+        }
+
+        @Override
+        public AccessibilityNodeInfo.AccessibilityAction createAccessibilityAction(
+                Context context) {
+            if (mSplitA11yInfo != null && mSplitA11yInfo.containsMultipleTasks) {
+                String accessibilityLabel = context.getString(R.string.split_app_info_accessibility,
+                        mSplitA11yInfo.taskTitle);
+                return new AccessibilityNodeInfo.AccessibilityAction(mAccessibilityActionId,
+                        accessibilityLabel);
+            } else {
+                return super.createAccessibilityAction(context);
+            }
+        }
+
         @Override
         public void onClick(View view) {
             dismissTaskMenuView(mTarget);
@@ -153,6 +185,19 @@
             mTarget.getStatsLogManager().logger().withItemInfo(mItemInfo)
                     .log(LAUNCHER_SYSTEM_SHORTCUT_APP_INFO_TAP);
         }
+
+        public static class SplitAccessibilityInfo {
+            public final boolean containsMultipleTasks;
+            public final CharSequence taskTitle;
+            public final int nodeId;
+
+            public SplitAccessibilityInfo(boolean containsMultipleTasks,
+                    CharSequence taskTitle, int nodeId) {
+                this.containsMultipleTasks = containsMultipleTasks;
+                this.taskTitle = taskTitle;
+                this.nodeId = nodeId;
+            }
+        }
     }
 
     public static final Factory<BaseDraggingActivity> INSTALL = (activity, itemInfo) -> {
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 62b8a48..673b011 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -122,7 +122,6 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String TASKBAR_WINDOW_CRASH = "b/201305599";
     public static final String TASK_VIEW_ID_CRASH = "b/195430732";
     public static final String NO_DROP_TARGET = "b/195031154";
     public static final String NULL_INT_SET = "b/200572078";
diff --git a/tests/Android.bp b/tests/Android.bp
index 3670c37..c329ece 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -23,7 +23,7 @@
 // Source code used for test
 filegroup {
     name: "launcher-tests-src",
-    srcs: ["src/**/*.java"],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
 }
 
 // Source code used for oop test helpers
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
deleted file mode 100644
index 8a4590a..0000000
--- a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.android.launcher3.model;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.util.Pair;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.model.BgDataModel.Callbacks;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.util.ContentWriter;
-import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.GridOccupancy;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.IntSparseArrayMap;
-import com.android.launcher3.util.LauncherModelHelper;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Tests for {@link AddWorkspaceItemsTask}
- */
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AddWorkspaceItemsTaskTest {
-
-    private final ComponentName mComponent1 = new ComponentName("a", "b");
-    private final ComponentName mComponent2 = new ComponentName("b", "b");
-
-    private Context mTargetContext;
-    private InvariantDeviceProfile mIdp;
-    private LauncherAppState mAppState;
-    private LauncherModelHelper mModelHelper;
-
-    private IntArray mExistingScreens;
-    private IntArray mNewScreens;
-    private IntSparseArrayMap<GridOccupancy> mScreenOccupancy;
-
-    @Before
-    public void setup() {
-        mModelHelper = new LauncherModelHelper();
-        mTargetContext = mModelHelper.sandboxContext;
-        mIdp = InvariantDeviceProfile.INSTANCE.get(mTargetContext);
-        mIdp.numColumns = mIdp.numRows = 5;
-        mAppState = LauncherAppState.getInstance(mTargetContext);
-
-        mExistingScreens = new IntArray();
-        mScreenOccupancy = new IntSparseArrayMap<>();
-        mNewScreens = new IntArray();
-    }
-
-    @After
-    public void tearDown() {
-        mModelHelper.destroy();
-    }
-
-    private AddWorkspaceItemsTask newTask(ItemInfo... items) {
-        List<Pair<ItemInfo, Object>> list = new ArrayList<>();
-        for (ItemInfo item : items) {
-            list.add(Pair.create(item, null));
-        }
-        return new AddWorkspaceItemsTask(list);
-    }
-
-    @Test
-    public void testFindSpaceForItem_prefers_second() throws Exception {
-        // First screen has only one hole of size 1
-        int nextId = setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
-
-        // Second screen has 2 holes of sizes 3x2 and 2x3
-        setupWorkspaceWithHoles(nextId, 2, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
-
-        int[] spaceFound = newTask().findSpaceForItem(
-                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 1, 1);
-        assertEquals(1, spaceFound[0]);
-        assertTrue(mScreenOccupancy.get(spaceFound[0])
-                .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1));
-
-        // Find a larger space
-        spaceFound = newTask().findSpaceForItem(
-                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 2, 3);
-        assertEquals(2, spaceFound[0]);
-        assertTrue(mScreenOccupancy.get(spaceFound[0])
-                .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3));
-    }
-
-    @Test
-    public void testFindSpaceForItem_adds_new_screen() throws Exception {
-        // First screen has 2 holes of sizes 3x2 and 2x3
-        setupWorkspaceWithHoles(1, 1, new Rect(2, 0, 5, 2), new Rect(0, 2, 2, 5));
-
-        IntArray oldScreens = mExistingScreens.clone();
-        int[] spaceFound = newTask().findSpaceForItem(
-                mAppState, mModelHelper.getBgDataModel(), mExistingScreens, mNewScreens, 3, 3);
-        assertFalse(oldScreens.contains(spaceFound[0]));
-        assertTrue(mNewScreens.contains(spaceFound[0]));
-    }
-
-    @Test
-    public void testAddItem_existing_item_ignored() throws Exception {
-        WorkspaceItemInfo info = new WorkspaceItemInfo();
-        info.intent = new Intent().setComponent(mComponent1);
-
-        // Setup a screen with a hole
-        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
-
-        // Nothing was added
-        assertTrue(mModelHelper.executeTaskForTest(newTask(info)).isEmpty());
-    }
-
-    @Test
-    public void testAddItem_some_items_added() throws Exception {
-        Callbacks callbacks = mock(Callbacks.class);
-        Executors.MAIN_EXECUTOR.submit(() -> mModelHelper.getModel().addCallbacks(callbacks)).get();
-
-        WorkspaceItemInfo info = new WorkspaceItemInfo();
-        info.intent = new Intent().setComponent(mComponent1);
-
-        WorkspaceItemInfo info2 = new WorkspaceItemInfo();
-        info2.intent = new Intent().setComponent(mComponent2);
-
-        // Setup a screen with a hole
-        setupWorkspaceWithHoles(1, 1, new Rect(2, 2, 3, 3));
-
-        mModelHelper.executeTaskForTest(newTask(info, info2)).get(0).run();
-        ArgumentCaptor<ArrayList> notAnimated = ArgumentCaptor.forClass(ArrayList.class);
-        ArgumentCaptor<ArrayList> animated = ArgumentCaptor.forClass(ArrayList.class);
-
-        // only info2 should be added because info was already added to the workspace
-        // in setupWorkspaceWithHoles()
-        verify(callbacks).bindAppsAdded(any(IntArray.class), notAnimated.capture(),
-                animated.capture());
-        assertTrue(notAnimated.getValue().isEmpty());
-
-        assertEquals(1, animated.getValue().size());
-        assertTrue(animated.getValue().contains(info2));
-    }
-
-    private int setupWorkspaceWithHoles(int startId, int screenId, Rect... holes) throws Exception {
-        return mModelHelper.executeSimpleTask(
-                model -> writeWorkspaceWithHoles(model, startId, screenId, holes));
-    }
-
-    private int writeWorkspaceWithHoles(
-            BgDataModel bgDataModel, int startId, int screenId, Rect... holes) {
-        GridOccupancy occupancy = new GridOccupancy(mIdp.numColumns, mIdp.numRows);
-        occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true);
-        for (Rect r : holes) {
-            occupancy.markCells(r, false);
-        }
-
-        mExistingScreens.add(screenId);
-        mScreenOccupancy.append(screenId, occupancy);
-
-        for (int x = 0; x < mIdp.numColumns; x++) {
-            for (int y = 0; y < mIdp.numRows; y++) {
-                if (!occupancy.cells[x][y]) {
-                    continue;
-                }
-
-                WorkspaceItemInfo info = new WorkspaceItemInfo();
-                info.intent = new Intent().setComponent(mComponent1);
-                info.id = startId++;
-                info.screenId = screenId;
-                info.cellX = x;
-                info.cellY = y;
-                info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
-                bgDataModel.addItem(mTargetContext, info, false);
-
-                ContentWriter writer = new ContentWriter(mTargetContext);
-                info.writeToValues(writer);
-                writer.put(Favorites._ID, info.id);
-                mTargetContext.getContentResolver().insert(Favorites.CONTENT_URI,
-                        writer.getValues(mTargetContext));
-            }
-        }
-        return startId;
-    }
-}
diff --git a/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
new file mode 100644
index 0000000..e315658
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/AddWorkspaceItemsTaskTest.kt
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2021 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.launcher3.model
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.util.Pair
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.*
+import com.android.launcher3.util.IntArray
+import org.junit.After
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.*
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.verify
+import kotlin.collections.ArrayList
+
+/**
+ * Tests for [AddWorkspaceItemsTask]
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AddWorkspaceItemsTaskTest {
+
+    @Captor
+    private lateinit var animatedItemArgumentCaptor: ArgumentCaptor<ArrayList<ItemInfo>>
+
+    @Captor
+    private lateinit var notAnimatedItemArgumentCaptor: ArgumentCaptor<ArrayList<ItemInfo>>
+
+    @Mock
+    private lateinit var dataModelCallbacks: BgDataModel.Callbacks
+
+    private lateinit var mTargetContext: Context
+    private lateinit var mIdp: InvariantDeviceProfile
+    private lateinit var mAppState: LauncherAppState
+    private lateinit var mModelHelper: LauncherModelHelper
+    private lateinit var mExistingScreens: IntArray
+    private lateinit var mNewScreens: IntArray
+    private lateinit var mScreenOccupancy: IntSparseArrayMap<GridOccupancy>
+
+    private val emptyScreenHoles = listOf(Rect(0, 0, 5, 5))
+    private val fullScreenHoles = emptyList<Rect>()
+
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        mModelHelper = LauncherModelHelper()
+        mTargetContext = mModelHelper.sandboxContext
+        mIdp = InvariantDeviceProfile.INSTANCE[mTargetContext]
+        mIdp.numRows = 5
+        mIdp.numColumns = mIdp.numRows
+        mAppState = LauncherAppState.getInstance(mTargetContext)
+        mExistingScreens = IntArray()
+        mScreenOccupancy = IntSparseArrayMap()
+        mNewScreens = IntArray()
+        Executors.MAIN_EXECUTOR.submit { mModelHelper.model.addCallbacks(dataModelCallbacks) }.get()
+    }
+
+    @After
+    fun tearDown() {
+        mModelHelper.destroy()
+    }
+
+    @Test
+    fun justEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnFirstScreenId() {
+        setupWorkspacesWithHoles(
+                screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole
+                //  2 holes of sizes 3x2 and 2x3
+                screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)),
+        )
+
+        val spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 1, 1)
+        assertEquals(1, spaceFound[0])
+        assertTrue(mScreenOccupancy[spaceFound[0]]
+                .isRegionVacant(spaceFound[1], spaceFound[2], 1, 1))
+    }
+
+    @Test
+    fun notEnoughSpaceOnFirstScreen_whenFindSpaceForItem_thenReturnSecondScreenId() {
+        setupWorkspacesWithHoles(
+                screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole
+                //  2 holes of sizes 3x2 and 2x3
+                screen2 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)),
+        )
+
+        // Find a larger space
+        val spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 2, 3)
+        assertEquals(2, spaceFound[0])
+        assertTrue(mScreenOccupancy[spaceFound[0]]
+                .isRegionVacant(spaceFound[1], spaceFound[2], 2, 3))
+    }
+
+    @Test
+    fun notEnoughSpaceOnExistingScreens_whenFindSpaceForItem_thenReturnNewScreenId() {
+        setupWorkspacesWithHoles(
+                //  2 holes of sizes 3x2 and 2x3
+                screen1 = listOf(Rect(2, 0, 5, 2), Rect(0, 2, 2, 5)),
+                //  2 holes of sizes 1x2 and 2x2
+                screen2 = listOf(Rect(1, 0, 2, 2), Rect(3, 2, 5, 4)),
+        )
+
+        val oldScreens = mExistingScreens.clone()
+        val spaceFound = newTask().findSpaceForItem(
+                mAppState, mModelHelper.bgDataModel, mExistingScreens, mNewScreens, 3, 3)
+        assertFalse(oldScreens.contains(spaceFound[0]))
+        assertTrue(mNewScreens.contains(spaceFound[0]))
+    }
+
+    @Test
+    fun enoughSpaceOnFirstScreen_whenTaskRuns_thenAddItemToFirstScreen() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 space
+                screen2 = listOf(Rect(2, 0, 5, 2)), // 3x2 space
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(1, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun firstPageIsFull_whenTaskRuns_thenAddItemToSecondScreen() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = fullScreenHoles,
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(2, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun firstScreenIsEmptyButSecondIsNotEmpty_whenTaskRuns_thenAddItemToSecondScreen() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = emptyScreenHoles,
+                screen2 = listOf(Rect(2, 0, 5, 2)), // 3x2 space
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(2, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun twoEmptyMiddleScreens_whenTaskRuns_thenAddItemToThirdScreen() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = emptyScreenHoles,
+                screen2 = emptyScreenHoles,
+                screen3 = listOf(Rect(1, 1, 4, 4)), // 3x3 space
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(3, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun allPagesAreFull_whenTaskRuns_thenAddItemToNewScreen() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = fullScreenHoles,
+                screen2 = fullScreenHoles,
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(3, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun firstTwoPagesAreFull_and_ThirdPageIsEmpty_whenTaskRuns_thenAddItemToThirdPage() {
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = fullScreenHoles,
+                screen2 = fullScreenHoles,
+                screen3 = emptyScreenHoles
+        )
+        val addedItems = testAddItems(workspaceHoles, getNewItem())
+        assertEquals(1, addedItems.size)
+        assertEquals(3, addedItems.first().itemInfo.screenId)
+    }
+
+    @Test
+    fun itemIsAlreadyAdded_whenTaskRun_thenIgnoreItem() {
+        val task = newTask(getExistingItem())
+        setupWorkspacesWithHoles(
+                screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole
+        )
+
+        // Nothing was added
+        assertTrue(mModelHelper.executeTaskForTest(task).isEmpty())
+    }
+
+    @Test
+    fun newAndExistingItems_whenTaskRun_thenAddOnlyTheNewOne() {
+        val newItem = getNewItem()
+        val workspaceHoles = createWorkspaceHoles(
+                screen1 = listOf(Rect(2, 2, 3, 3)), // 1x1 hole
+        )
+        val addedItems = testAddItems(workspaceHoles, getExistingItem(), newItem)
+        assertEquals(1, addedItems.size)
+        val addedItem = addedItems.first()
+        assert(addedItem.isAnimated)
+        val addedItemInfo = addedItem.itemInfo
+        assertEquals(1, addedItemInfo.screenId)
+        assertEquals(newItem, addedItemInfo)
+    }
+
+    private fun testAddItems(
+            workspaceHoles: List<List<Rect>>,
+            vararg itemsToAdd: WorkspaceItemInfo
+    ): List<AddedItem> {
+        setupWorkspaces(workspaceHoles)
+        mModelHelper.executeTaskForTest(newTask(*itemsToAdd))[0].run()
+
+        verify(dataModelCallbacks).bindAppsAdded(any(),
+                notAnimatedItemArgumentCaptor.capture(), animatedItemArgumentCaptor.capture())
+
+        val addedItems = mutableListOf<AddedItem>()
+        addedItems.addAll(animatedItemArgumentCaptor.value.map { AddedItem(it, true) })
+        addedItems.addAll(notAnimatedItemArgumentCaptor.value.map { AddedItem(it, false) })
+        return addedItems
+    }
+
+    private fun setupWorkspaces(workspaceHoles: List<List<Rect>>) {
+        var nextItemId = 1
+        var screenId = 1
+        workspaceHoles.forEach { holes ->
+            nextItemId = setupWorkspace(nextItemId, screenId++, *holes.toTypedArray())
+        }
+    }
+
+    private fun setupWorkspace(startId: Int, screenId: Int, vararg holes: Rect): Int {
+        return mModelHelper.executeSimpleTask { dataModel ->
+            writeWorkspaceWithHoles(dataModel, startId, screenId, *holes)
+        }
+    }
+
+    private fun writeWorkspaceWithHoles(
+            bgDataModel: BgDataModel,
+            itemStartId: Int,
+            screenId: Int,
+            vararg holes: Rect,
+    ): Int {
+        var itemId = itemStartId
+        val occupancy = GridOccupancy(mIdp.numColumns, mIdp.numRows)
+        occupancy.markCells(0, 0, mIdp.numColumns, mIdp.numRows, true)
+        holes.forEach { holeRect ->
+            occupancy.markCells(holeRect, false)
+        }
+        mExistingScreens.add(screenId)
+        mScreenOccupancy.append(screenId, occupancy)
+        for (x in 0 until mIdp.numColumns) {
+            for (y in 0 until mIdp.numRows) {
+                if (!occupancy.cells[x][y]) {
+                    continue
+                }
+                val info = getExistingItem()
+                info.id = itemId++
+                info.screenId = screenId
+                info.cellX = x
+                info.cellY = y
+                info.container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+                bgDataModel.addItem(mTargetContext, info, false)
+                val writer = ContentWriter(mTargetContext)
+                info.writeToValues(writer)
+                writer.put(LauncherSettings.Favorites._ID, info.id)
+                mTargetContext.contentResolver.insert(LauncherSettings.Favorites.CONTENT_URI,
+                        writer.getValues(mTargetContext))
+            }
+        }
+        return itemId
+    }
+
+    private fun setupWorkspacesWithHoles(
+            screen1: List<Rect>? = null,
+            screen2: List<Rect>? = null,
+            screen3: List<Rect>? = null,
+    ) = createWorkspaceHoles(screen1, screen2, screen3)
+            .let(this::setupWorkspaces)
+
+    private fun createWorkspaceHoles(
+            screen1: List<Rect>? = null,
+            screen2: List<Rect>? = null,
+            screen3: List<Rect>? = null,
+    ): List<List<Rect>> = listOfNotNull(screen1, screen2, screen3)
+
+    private fun newTask(vararg items: ItemInfo): AddWorkspaceItemsTask =
+            items.map { Pair.create(it, Any()) }
+                    .toMutableList()
+                    .let(::AddWorkspaceItemsTask)
+
+    private fun getExistingItem() = WorkspaceItemInfo()
+            .apply { intent = Intent().setComponent(ComponentName("a", "b")) }
+
+    private fun getNewItem() = WorkspaceItemInfo()
+            .apply { intent = Intent().setComponent(ComponentName("b", "b")) }
+}
+
+private data class AddedItem(
+        val itemInfo: ItemInfo,
+        val isAnimated: Boolean
+)
\ No newline at end of file