Merge "Bubble bar flyout polish" into main
diff --git a/quickstep/res/values-en-rGB/strings.xml b/quickstep/res/values-en-rGB/strings.xml
index a74a17b..04b04dd 100644
--- a/quickstep/res/values-en-rGB/strings.xml
+++ b/quickstep/res/values-en-rGB/strings.xml
@@ -139,8 +139,7 @@
     <string name="always_show_taskbar" msgid="3608801276107751229">"Always show Taskbar"</string>
     <string name="change_navigation_mode" msgid="9088393078736808968">"Change navigation mode"</string>
     <string name="taskbar_divider_a11y_title" msgid="6608690309720242080">"Taskbar divider"</string>
-    <!-- no translation found for taskbar_overflow_a11y_title (7960342079198820179) -->
-    <skip />
+    <string name="taskbar_overflow_a11y_title" msgid="7960342079198820179">"Taskbar overflow"</string>
     <string name="move_drop_target_top_or_left" msgid="2988702185049595807">"Move to top/left"</string>
     <string name="move_drop_target_bottom_or_right" msgid="5431393418797620162">"Move to bottom/right"</string>
     <string name="quick_switch_overflow" msgid="3679780650881041632">"{count,plural, =1{more app}other{more apps}}"</string>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 065646a..43b1ae0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_OVERVIEW;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_FOR_BUBBLES;
+import static com.android.launcher3.taskbar.TaskbarStashController.UNLOCK_TRANSITION_MEMOIZATION_MS;
 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
 import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS;
 import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS;
@@ -167,7 +168,12 @@
     private boolean mSkipNextRecentsAnimEnd;
 
     // Time when FLAG_TASKBAR_HIDDEN was last cleared, SystemClock.elapsedRealtime (milliseconds).
-    private long mLastUnlockTimeMs = 0;
+    private long mLastRemoveTaskbarHiddenTimeMs = 0;
+    /**
+     * Time when FLAG_DEVICE_LOCKED was last cleared, plus
+     * {@link TaskbarStashController#UNLOCK_TRANSITION_MEMOIZATION_MS}
+     */
+    private long mLastUnlockTransitionTimeout;
 
     private @Nullable TaskBarRecentsAnimationListener mTaskBarRecentsAnimationListener;
 
@@ -535,7 +541,7 @@
 
         if (hasAnyFlag(changedFlags, FLAG_TASKBAR_HIDDEN) && !hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
             // Take note of the current time, as the taskbar is made visible again.
-            mLastUnlockTimeMs = SystemClock.elapsedRealtime();
+            mLastRemoveTaskbarHiddenTimeMs = SystemClock.elapsedRealtime();
         }
 
         boolean isHidden = hasAnyFlag(FLAG_TASKBAR_HIDDEN);
@@ -561,7 +567,8 @@
                 // with a fingerprint reader. This should only be done when the device was woken
                 // up via fingerprint reader, however since this information is currently not
                 // available, opting to always delay the fade-in a bit.
-                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime() - mLastUnlockTimeMs;
+                long durationSinceLastUnlockMs = SystemClock.elapsedRealtime()
+                        - mLastRemoveTaskbarHiddenTimeMs;
                 taskbarVisibility.setStartDelay(
                         Math.max(0, TASKBAR_SHOW_DELAY_MS - durationSinceLastUnlockMs));
             }
@@ -631,6 +638,15 @@
         boolean isUnlockTransition =
                 hasAnyFlag(changedFlags, FLAG_DEVICE_LOCKED) && !hasAnyFlag(FLAG_DEVICE_LOCKED);
         if (isUnlockTransition) {
+            // the launcher might not be resumed at the time the device is considered
+            // unlocked (when the keyguard goes away), but possibly shortly afterwards.
+            // To play the unlock transition at the time the unstash animation actually happens,
+            // this memoizes the state transition for UNLOCK_TRANSITION_MEMOIZATION_MS.
+            mLastUnlockTransitionTimeout =
+                    SystemClock.elapsedRealtime() + UNLOCK_TRANSITION_MEMOIZATION_MS;
+        }
+        boolean isInUnlockTimeout = SystemClock.elapsedRealtime() < mLastUnlockTransitionTimeout;
+        if (isUnlockTransition || isInUnlockTimeout) {
             // When transitioning to unlocked, ensure the hotseat is fully visible from the
             // beginning. The hotseat itself is animated by LauncherUnlockAnimationController.
             mIconAlignment.cancelAnimation();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index 6222e53..8991965 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -191,7 +191,7 @@
 
     // Duration for which an unlock event is considered "current", as other events are received
     // asynchronously.
-    private static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
+    public static final long UNLOCK_TRANSITION_MEMOIZATION_MS = 200;
 
     /**
      * The default stash animation, morphing the taskbar into the navbar.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index b495e7d..e522035 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -329,7 +329,8 @@
      */
     public void setRecentsButtonDisabled(boolean isDisabled) {
         // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha.
-        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1);
+        mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).animateToValue(isDisabled ? 0 : 1)
+                .start();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 235ec7b..111069f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -155,6 +155,9 @@
                 0,
                 timings.getGridSlideSecondaryInterpolator());
 
+        mRecentsView.handleDesktopTaskInSplitSelectState(builder,
+                timings.getDesktopTaskFadeInterpolator());
+
         if (!animate) {
             AnimatorSet as = builder.buildAnim();
             as.start();
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 9ad6b303..dc7ed24 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1149,12 +1149,18 @@
 
         if (endTarget != NEW_TASK) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
         }
         if (endTarget != HOME) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_CLOSE_TO_HOME);
         }
         if (endTarget != RECENTS) {
             InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
+        } else {
+            InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
         }
 
         switch (endTarget) {
diff --git a/quickstep/src/com/android/quickstep/FocusState.kt b/quickstep/src/com/android/quickstep/FocusState.kt
new file mode 100644
index 0000000..ba3991f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/FocusState.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep
+
+import android.os.RemoteException
+import android.util.Log
+import android.view.Display.DEFAULT_DISPLAY
+import com.android.launcher3.util.Executors
+import com.android.wm.shell.shared.IFocusTransitionListener.Stub
+import com.android.wm.shell.shared.IShellTransitions
+
+/** Class to track focus state of displays and windows */
+class FocusState {
+
+    var focusedDisplayId = DEFAULT_DISPLAY
+        private set
+
+    private var listeners = mutableSetOf<FocusChangeListener>()
+
+    fun addListener(l: FocusChangeListener) = listeners.add(l)
+
+    fun removeListener(l: FocusChangeListener) = listeners.remove(l)
+
+    fun init(transitions: IShellTransitions?) {
+        try {
+            transitions?.setFocusTransitionListener(
+                object : Stub() {
+                    override fun onFocusedDisplayChanged(displayId: Int) {
+                        Executors.MAIN_EXECUTOR.execute {
+                            listeners.forEach { it.onFocusedDisplayChanged(displayId) }
+                        }
+                    }
+                }
+            )
+        } catch (e: RemoteException) {
+            Log.w(TAG, "Failed call setFocusTransitionListener", e)
+        }
+    }
+
+    interface FocusChangeListener {
+        fun onFocusedDisplayChanged(displayId: Int)
+    }
+
+    override fun toString() = "{FocusState focusedDisplayId=$focusedDisplayId}"
+
+    companion object {
+        private const val TAG = "FocusState"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 34435d5..a55cf18 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -161,6 +161,7 @@
     private IRemoteAnimationRunner mBackToLauncherRunner;
     private IDragAndDrop mDragAndDrop;
     private final HomeVisibilityState mHomeVisibilityState = new HomeVisibilityState();
+    private final FocusState mFocusState = new FocusState();
 
     // Used to dedupe calls to SystemUI
     private int mLastShelfHeight;
@@ -300,6 +301,7 @@
         registerSplitScreenListener(mSplitScreenListener);
         registerSplitSelectListener(mSplitSelectListener);
         mHomeVisibilityState.init(mShellTransitions);
+        mFocusState.init(mShellTransitions);
         setStartingWindowListener(mStartingWindowListener);
         setLauncherUnlockAnimationController(
                 mLauncherActivityClass, mLauncherUnlockAnimationController);
@@ -1144,6 +1146,10 @@
         return mHomeVisibilityState;
     }
 
+    public FocusState getFocusState() {
+        return mFocusState;
+    }
+
     /**
      * Returns a surface which can be used to attach overlays to home task or null if
      * the task doesn't exist or sysui is not connected
@@ -1592,6 +1598,7 @@
         pw.println("\tmOneHanded=" + mOneHanded);
         pw.println("\tmShellTransitions=" + mShellTransitions);
         pw.println("\tmHomeVisibilityState=" + mHomeVisibilityState);
+        pw.println("\tmFocusState=" + mFocusState);
         pw.println("\tmStartingWindow=" + mStartingWindow);
         pw.println("\tmStartingWindowListener=" + mStartingWindowListener);
         pw.println("\tmSysuiUnlockAnimationController=" + mSysuiUnlockAnimationController);
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 5ad55ae..49bff8d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -228,10 +228,13 @@
         }
 
         float velocityYPxPerS = mVelocityTracker.getYVelocity();
+        float dY = Math.abs(mLastPos.y - mDownPos.y);
         if (mCanPlayTaskbarBgAlphaAnimation
                 && mMotionMoveCount >= NUM_MOTION_MOVE_THRESHOLD // Arbitrary value
                 && velocityYPxPerS != 0 // Ignore these
-                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold) {
+                && velocityYPxPerS >= mTaskbarSlowVelocityYThreshold
+                && dY != 0
+                && dY > mTouchSlop) {
             mTaskbarActivityContext.playTaskbarBackgroundAlphaAnimation();
             mCanPlayTaskbarBgAlphaAnimation = false;
         }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 5028da4..9510a05 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -52,7 +52,6 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.StyleRes;
 import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.content.res.AppCompatResources;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
@@ -610,8 +609,8 @@
 
     private void updateDrawables() {
         if (mContext != null) {
-            mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockWallpaperResId()));
+            mTutorialFragment.getRootView()
+                    .setBackground(mContext.getDrawable(getMockWallpaperResId()));
             mTutorialFragment.updateFeedbackAnimation();
             mFakeLauncherView.setBackgroundColor(getFakeLauncherColor());
             updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId());
@@ -619,9 +618,7 @@
             mFakeTaskView.animate().alpha(1).setListener(
                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
             mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor());
-            mFakeIconView.setBackground(AppCompatResources.getDrawable(
-                    mContext, getMockAppIconResId()));
-
+            mFakeIconView.setBackground(mContext.getDrawable(getMockAppIconResId()));
             mExitingAppView.setBackgroundColor(getExitingAppColor());
             mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
             updateHotseatChildViewColor(mHotseatIconView);
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
index ae0e725..f1fc179 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialStepIndicator.java
@@ -22,8 +22,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.GraphicsUtils;
@@ -91,9 +89,8 @@
         int inactiveStepIndicatorColor = GraphicsUtils.getAttrColor(
                 getContext(), android.R.attr.textColorSecondaryInverse);
         for (int i = 0; i < mTotalSteps; i++) {
-            Drawable pageIndicatorPillDrawable = AppCompatResources.getDrawable(
-                    getContext(), R.drawable.tutorial_step_indicator_pill);
-
+            Drawable pageIndicatorPillDrawable =
+                    getContext().getDrawable(R.drawable.tutorial_step_indicator_pill);
             if (i >= getChildCount()) {
                 ImageView pageIndicatorPill = new ImageView(getContext());
                 pageIndicatorPill.setImageDrawable(pageIndicatorPillDrawable);
diff --git a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
index aa0fd88..c3b072d 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/RecentsViewUtils.kt
@@ -73,9 +73,21 @@
      * Returns the first TaskView that should be displayed as a large tile.
      *
      * @param taskViews List of [TaskView]s
+     * @param splitSelectActive current split state
      */
-    fun getFirstLargeTaskView(taskViews: Iterable<TaskView>): TaskView? =
-        taskViews.firstOrNull { it.isLargeTile }
+    fun getFirstLargeTaskView(
+        taskViews: MutableIterable<TaskView>,
+        splitSelectActive: Boolean,
+    ): TaskView? =
+        taskViews.firstOrNull { it.isLargeTile && !(splitSelectActive && it is DesktopTaskView) }
+
+    /**
+     * Returns the first TaskView that is not large
+     *
+     * @param taskViews List of [TaskView]s
+     */
+    fun getFirstSmallTaskView(taskViews: MutableIterable<TaskView>): TaskView? =
+        taskViews.firstOrNull { !it.isLargeTile }
 
     /** Returns the last TaskView that should be displayed as a large tile. */
     fun getLastLargeTaskView(taskViews: Iterable<TaskView>): TaskView? =
@@ -101,6 +113,12 @@
             it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
         }
 
+    /** Returns if any small tasks are fully visible */
+    fun isAnySmallTaskFullyVisible(
+        taskViews: Iterable<TaskView>,
+        isTaskViewFullyVisible: (TaskView) -> Boolean,
+    ): Boolean = taskViews.any { !it.isLargeTile && isTaskViewFullyVisible(it) }
+
     /** Returns the current list of [TaskView] children. */
     fun getTaskViews(taskViewCount: Int, requireTaskViewAt: (Int) -> TaskView): Iterable<TaskView> =
         (0 until taskViewCount).map(requireTaskViewAt)
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 3449cf2..f708f4b 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -238,7 +238,7 @@
                 taskViewHeight,
             )
         val snapshotViewSize =
-            if (isPrimaryTaskSplitting) primarySnapshotViewSize else secondarySnapshotViewSize
+            if (isPrimaryTaskSplitting) secondarySnapshotViewSize else primarySnapshotViewSize
         if (deviceProfile.isLeftRightSplit) {
             // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
             val centerThumbnailTranslationX: Float = (taskViewWidth - snapshotViewSize.x) / 2f
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
index b618546..90569b4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
@@ -105,6 +105,10 @@
     default Interpolator getGridSlidePrimaryInterpolator() { return LINEAR; }
     default Interpolator getGridSlideSecondaryInterpolator() { return LINEAR; }
 
+    default Interpolator getDesktopTaskFadeInterpolator() {
+        return LINEAR;
+    }
+
     // Defaults for HomeToSplit
     default float getScrimFadeInStartOffset() { return 0; }
     default float getScrimFadeInEndOffset() { return 0; }
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 60c6ade..8f579e2 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -743,6 +743,7 @@
      */
     public void resetState() {
         mSplitSelectDataHolder.resetState();
+        mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
         dispatchOnSplitSelectionExit();
         mRecentsAnimationRunning = false;
         mLaunchingTaskView = null;
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index c7777d8..f5be103 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -305,6 +305,14 @@
     }
 
     /**
+     * Override the pivot used to apply scale changes.
+     */
+    public void setPivotOverride(PointF pivotOverride) {
+        mPivotOverride = pivotOverride;
+        getFullScreenScale();
+    }
+
+    /**
      * Adds animation for all the components corresponding to transition from an app to overview.
      */
     public void addAppToOverviewAnim(PendingAnimation pa, Interpolator interpolator) {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a075c58..82f08e5 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1706,10 +1706,11 @@
                     return;
                 }
                 TaskView taskView = getTaskViewAt(mNextPage);
-                // Snap to fully visible focused task and clear all button.
                 boolean shouldSnapToLargeTask = taskView != null && taskView.isLargeTile()
-                        && isTaskViewFullyVisible(taskView);
+                        && !mUtils.isAnySmallTaskFullyVisible(getTaskViews(),
+                        this::isTaskViewFullyVisible);
                 boolean shouldSnapToClearAll = mNextPage == indexOfChild(mClearAllButton);
+                // Snap to large tile when grid tasks aren't fully visible or the clear all button.
                 if (!shouldSnapToLargeTask && !shouldSnapToClearAll) {
                     return;
                 }
@@ -3140,7 +3141,7 @@
         // Horizontal grid translation for each task
         float[] gridTranslations = new float[taskCount];
 
-        int focusedTaskIndex = Integer.MAX_VALUE;
+        int lastLargeTaskIndex = Integer.MAX_VALUE;
         Set<Integer> largeTasksIndices = new HashSet<>();
         int focusedTaskShift = 0;
         int largeTaskWidthAndSpacing = 0;
@@ -3163,8 +3164,12 @@
             boolean isLargeTile = taskView.isLargeTile();
 
             if (isLargeTile) {
-                topRowWidth += taskWidthAndSpacing;
-                bottomRowWidth += taskWidthAndSpacing;
+                // DesktopTaskView`s are hidden during split select state, so we shouldn't count
+                // them when calculating row width.
+                if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) {
+                    topRowWidth += taskWidthAndSpacing;
+                    bottomRowWidth += taskWidthAndSpacing;
+                }
                 gridTranslations[i] += focusedTaskShift;
                 gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
 
@@ -3172,9 +3177,7 @@
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
-                if (taskView.getTaskViewId() == mFocusedTaskViewId) {
-                    focusedTaskIndex = i;
-                }
+                lastLargeTaskIndex = i;
                 largeTasksIndices.add(i);
                 largeTaskWidthAndSpacing = taskWidthAndSpacing;
 
@@ -3183,8 +3186,8 @@
                     snappedTaskRowWidth = taskWidthAndSpacing;
                 }
             } else {
-                if (i > focusedTaskIndex) {
-                    // For tasks after the focused task, shift by focused task's width and spacing.
+                if (i > lastLargeTaskIndex) {
+                    // For tasks after the last large task, shift by large task's width and spacing.
                     gridTranslations[i] +=
                             mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
                 } else {
@@ -3609,10 +3612,10 @@
         float dismissedTaskWidth = 0;
         float nextFocusedTaskWidth = 0;
 
-        // Non-grid specific properties.
         int[] oldScroll = new int[count];
         int[] newScroll = new int[count];
         int scrollDiffPerPage = 0;
+        // Non-grid specific properties.
         boolean needsCurveUpdates = false;
 
         if (showAsGrid) {
@@ -3642,13 +3645,13 @@
                     }
                 }
             }
-        } else {
-            getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
-            getPageScrolls(newScroll, false,
-                    v -> v.getVisibility() != GONE && v != dismissedTaskView);
-            if (count > 1) {
-                scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
-            }
+        }
+
+        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+        getPageScrolls(newScroll, false,
+                v -> v.getVisibility() != GONE && v != dismissedTaskView);
+        if (count > 1) {
+            scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
         }
 
         float dismissTranslationInterpolationEnd = 1;
@@ -3800,7 +3803,9 @@
                         addDismissedTaskAnimations(dismissedTaskView, duration, anim);
                     }
                 }
-            } else if (!showAsGrid) {
+            } else if (!showAsGrid || (enableLargeDesktopWindowingTile()
+                    && dismissedTaskView.isLargeTile()
+                    && nextFocusedTaskView == null)) {
                 int offset = getOffsetToDismissedTask(scrollDiffPerPage, dismissedIndex, taskCount);
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
@@ -3812,18 +3817,16 @@
                             splitTimings);
                     needsCurveUpdates = true;
                 }
-            } else if (child instanceof TaskView) {
-                TaskView taskView = (TaskView) child;
+            } else if (child instanceof TaskView taskView) {
                 if (isFocusedTaskDismissed) {
                     if (nextFocusedTaskView != null &&
                             !isSameGridRow(taskView, nextFocusedTaskView)) {
                         continue;
                     }
-                } else {
-                    if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
-                        continue;
-                    }
+                } else if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
+                    continue;
                 }
+
                 // Animate task with index >= dismissed index and in the same row as the
                 // dismissed index or next focused index. Offset successive task dismissal
                 // durations for a staggered effect.
@@ -4134,15 +4137,18 @@
      * - Dragging an adjacent page on the left side (right side for RTL)
      */
     private int getOffsetToDismissedTask(int scrollDiffPerPage, int dismissedIndex, int taskCount) {
+        // When mCurrentPage is ClearAllButton, use the last TaskView instead to calculate
+        // offset.
+        int currentPage = mCurrentPage == taskCount ? taskCount - 1 : mCurrentPage;
         int offset = mIsRtl ? scrollDiffPerPage : 0;
-        if (mCurrentPage == dismissedIndex) {
+        if (currentPage == dismissedIndex) {
             int lastPage = taskCount - 1;
-            if (mCurrentPage == lastPage) {
+            if (currentPage == lastPage) {
                 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
             }
         } else {
             // Dismissing an adjacent page.
-            int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
+            int negativeAdjacent = currentPage - 1; // (Right in RTL, left in LTR)
             if (dismissedIndex == negativeAdjacent) {
                 offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
             }
@@ -5024,6 +5030,35 @@
     }
 
     /**
+     * Animate DesktopTaskView(s) to hide in split select
+     */
+    public void handleDesktopTaskInSplitSelectState(PendingAnimation builder,
+            Interpolator deskTopFadeInterPolator) {
+        if (enableLargeDesktopWindowingTile()) {
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView instanceof DesktopTaskView) {
+                    builder.addFloat(taskView.getSplitAlphaProperty(),
+                            MULTI_PROPERTY_VALUE, 1f, 0f,
+                            deskTopFadeInterPolator);
+                }
+            }
+        }
+    }
+
+    /**
+     * While exiting from split mode, show all existing DesktopTaskViews.
+     */
+    public void resetDesktopTaskFromSplitSelectState() {
+        if (enableLargeDesktopWindowingTile()) {
+            for (TaskView taskView : getTaskViews()) {
+                if (taskView instanceof DesktopTaskView) {
+                    taskView.setSplitAlpha(1f);
+                }
+            }
+        }
+    }
+
+    /**
      * Modifies a PendingAnimation with the animations for entering split staging
      */
     public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) {
@@ -5387,6 +5422,13 @@
                             mTempPointF);
                     setPivotX(mTempPointF.x);
                     setPivotY(mTempPointF.y);
+                    runActionOnRemoteHandles(
+                            remoteTargetHandle -> {
+                                remoteTargetHandle.getTaskViewSimulator().setPivotOverride(
+                                        mTempPointF);
+                                remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(
+                                        false);
+                            });
                 }
             });
         } else if (!showAsGrid) {
@@ -5857,11 +5899,12 @@
         if (mShowAsGridLastOnLayout) {
             // For grid Overview, it always start if a large tile (focused task or desktop task) if
             // they exist, otherwise it start with the first task.
-            TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView(getTaskViews());
+            TaskView firstLargeTaskView = mUtils.getFirstLargeTaskView(getTaskViews(),
+                    isSplitSelectionActive());
             if (firstLargeTaskView != null) {
                 firstView = firstLargeTaskView;
             } else {
-                firstView = getTaskViewAt(0);
+                firstView = mUtils.getFirstSmallTaskView(getTaskViews());
             }
         } else {
             firstView = mUtils.getFirstTaskViewInCarousel(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index f513a82..cc64dba 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -402,6 +402,15 @@
         }
         get() = taskViewAlpha.get(ALPHA_INDEX_ATTACH).value
 
+    var splitAlpha
+        set(value) {
+            splitAlphaProperty.value = value
+        }
+        get() = splitAlphaProperty.value
+
+    val splitAlphaProperty: MultiPropertyFactory<View>.MultiProperty
+        get() = taskViewAlpha.get(ALPHA_INDEX_SPLIT)
+
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
         private set
@@ -606,6 +615,7 @@
     override fun onRecycle() {
         resetPersistentViewTransforms()
         attachAlpha = 1f
+        splitAlpha = 1f
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
         if (!enableRefactorTaskThumbnail()) {
@@ -1687,8 +1697,9 @@
 
         private const val ALPHA_INDEX_STABLE = 0
         private const val ALPHA_INDEX_ATTACH = 1
+        private const val ALPHA_INDEX_SPLIT = 2
 
-        private const val NUM_ALPHA_CHANNELS = 2
+        private const val NUM_ALPHA_CHANNELS = 3
 
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
index d4a3b3a..412bdeb 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashControllerTest.kt
@@ -29,6 +29,7 @@
 import com.android.launcher3.anim.AnimatedFloat
 import com.android.launcher3.taskbar.StashedHandleView
 import com.android.launcher3.taskbar.TaskbarInsetsController
+import com.android.launcher3.taskbar.TaskbarStashController
 import com.android.launcher3.taskbar.bubbles.BubbleBarView
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
 import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
@@ -36,6 +37,7 @@
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.wm.shell.shared.animation.PhysicsAnimator
 import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Rule
@@ -59,7 +61,7 @@
         const val BUBBLE_BAR_WIDTH = 200
         const val BUBBLE_BAR_HEIGHT = 100
         const val HOTSEAT_TRANSLATION_Y = -45f
-        const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE
+        const val TASK_BAR_TRANSLATION_Y = -TASKBAR_BOTTOM_SPACE.toFloat()
         const val HANDLE_VIEW_WIDTH = 150
         const val HANDLE_VIEW_HEIGHT = 4
         const val BUBBLE_BAR_STASHED_TRANSLATION_Y = -4.5f
@@ -196,6 +198,93 @@
     }
 
     @Test
+    fun updateStashedAndExpandedState_unstash_bubbleBarShown_stashedHandleHidden() {
+        // Given bubble bar has bubbles and is stashed
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        val bubbleInitialTranslation = bubbleView.translationY
+
+        // When unstash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+            )
+        }
+
+        // Wait until animations ends
+        advanceTimeBy(BubbleStashController.BAR_STASH_DURATION)
+        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
+
+        // Then check BubbleBarController is notified
+        verify(bubbleBarViewController).onStashStateChanging()
+        // Bubble bar is unstashed
+        assertThat(mTransientBubbleStashController.isStashed).isFalse()
+        assertThat(bubbleBarView.translationY).isEqualTo(TASK_BAR_TRANSLATION_Y)
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleX).isEqualTo(1f)
+        assertThat(bubbleBarView.scaleY).isEqualTo(1f)
+        assertThat(bubbleBarView.background.alpha).isEqualTo(255)
+        // Handle view is hidden
+        assertThat(stashedHandleView.translationY).isEqualTo(0)
+        assertThat(stashedHandleView.alpha).isEqualTo(0)
+        // Bubble view is reset
+        assertThat(bubbleView.translationY).isEqualTo(bubbleInitialTranslation)
+        assertThat(bubbleView.alpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_stash_animatesAlphaForBubblesAndBackgroundSeparately() {
+        // Given bubble bar has bubbles and is unstashed
+        mTransientBubbleStashController.isStashed = false
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When stash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = true,
+                expand = false,
+            )
+        }
+
+        // Stop after alpha starts
+        advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10)
+
+        // Bubble bar alpha is set to 1
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // We animate alpha for background and children separately
+        assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f))
+        assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255))
+        assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt())
+    }
+
+    @Test
+    fun updateStashedAndExpandedState_unstash_animatesAlphaForBubblesAndBackgroundSeparately() {
+        // Given bubble bar has bubbles and is stashed
+        mTransientBubbleStashController.isStashed = true
+        whenever(bubbleBarViewController.isHiddenForNoBubbles).thenReturn(false)
+
+        // When unstash
+        getInstrumentation().runOnMainSync {
+            mTransientBubbleStashController.updateStashedAndExpandedState(
+                stash = false,
+                expand = false,
+            )
+        }
+
+        // Stop after alpha starts
+        advanceTimeBy(TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY + 10)
+
+        // Bubble bar alpha is set to 1
+        assertThat(bubbleBarView.alpha).isEqualTo(1f)
+        // We animate alpha for background and children separately
+        assertThat(bubbleView.alpha).isIn(Range.open(0f, 1f))
+        assertThat(bubbleBarView.background.alpha).isIn(Range.open(0, 255))
+        assertThat(bubbleBarView.background.alpha).isNotEqualTo((bubbleView.alpha * 255f).toInt())
+    }
+
+    @Test
     fun isSysuiLockedSwitchedToFalseForOverview_unlockAnimationIsShown() {
         // Given screen is locked and bubble bar has bubbles
         getInstrumentation().runOnMainSync {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index 9d0b2ab..cb70694 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -36,6 +36,7 @@
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
 import com.android.quickstep.util.SplitSelectStateController.SplitFromDesktopController
+import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsViewContainer
 import com.android.systemui.shared.recents.model.Task
 import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50
@@ -50,6 +51,7 @@
 import org.mockito.Mockito.`when`
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import java.util.function.Consumer
@@ -66,6 +68,7 @@
     private val recentsModel: RecentsModel = mock()
     private val pendingIntent: PendingIntent = mock()
     private val splitFromDesktopController: SplitFromDesktopController = mock()
+    private val recentsView: RecentsView<*, *> = mock()
 
     private lateinit var splitSelectStateController: SplitSelectStateController
 
@@ -73,6 +76,7 @@
     private val nonPrimaryUserHandle = UserHandle(ActivityManager.RunningTaskInfo().userId + 10)
 
     private var taskIdCounter = 0
+
     private fun getUniqueId(): Int {
         return ++taskIdCounter
     }
@@ -90,7 +94,7 @@
                 statsLogManager,
                 systemUiProxy,
                 recentsModel,
-                null /*activityBackCallback*/
+                null, /*activityBackCallback*/
             )
     }
 
@@ -100,12 +104,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("hotdog", "juice"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -122,7 +126,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -141,12 +145,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pomegranate", "juice")
+                ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -159,12 +163,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
             }
@@ -175,7 +179,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -194,12 +198,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pomegranate", "juice")
+                ComponentName("pomegranate", "juice"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -216,7 +220,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -237,12 +241,12 @@
                 ComponentName(matchingPackage, matchingClass),
                 nonPrimaryUserHandle,
                 ComponentName("pomegranate", "juice"),
-                nonPrimaryUserHandle
+                nonPrimaryUserHandle,
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pumpkin", "pie"),
-                ComponentName("personal", "computer")
+                ComponentName("personal", "computer"),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask1)
@@ -255,12 +259,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals("userId mismatched", it[0].key.userId, nonPrimaryUserHandle.identifier)
                 assertEquals(it[0], groupTask1.task1)
@@ -272,7 +276,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonPrimaryUserComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -291,12 +295,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -309,12 +313,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
             }
@@ -325,7 +329,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -348,7 +352,7 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -363,12 +367,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[1].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[1].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[1], groupTask2.task2)
             }
@@ -379,7 +383,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(nonMatchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -401,7 +405,7 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -415,12 +419,12 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask2.task2)
                 assertNull("No tasks should have matched", it[1] /*task*/)
@@ -432,7 +436,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -452,12 +456,12 @@
         val groupTask1 =
             generateGroupTask(
                 ComponentName(matchingPackage, matchingClass),
-                ComponentName("pumpkin", "pie")
+                ComponentName("pumpkin", "pie"),
             )
         val groupTask2 =
             generateGroupTask(
                 ComponentName("pomegranate", "juice"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask2)
@@ -471,23 +475,23 @@
                 assertEquals(
                     "ComponentName package mismatched",
                     it[0].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[0].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[0], groupTask1.task1)
                 assertEquals(
                     "ComponentName package mismatched",
                     it[1].key.baseIntent.component?.packageName,
-                    matchingPackage
+                    matchingPackage,
                 )
                 assertEquals(
                     "ComponentName class mismatched",
                     it[1].key.baseIntent.component?.className,
-                    matchingClass
+                    matchingClass,
                 )
                 assertEquals(it[1], groupTask2.task2)
             }
@@ -498,7 +502,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent, matchingComponent),
                         false /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -524,12 +528,12 @@
         val groupTask2 =
             generateGroupTask(
                 ComponentName(matchingPackage2, matchingClass2),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val groupTask3 =
             generateGroupTask(
                 ComponentName("hotdog", "pie"),
-                ComponentName(matchingPackage, matchingClass)
+                ComponentName(matchingPackage, matchingClass),
             )
         val tasks: ArrayList<GroupTask> = ArrayList()
         tasks.add(groupTask3)
@@ -550,7 +554,7 @@
                     splitSelectStateController.findLastActiveTasksAndRunCallback(
                         listOf(matchingComponent2, matchingComponent),
                         true /* findExactPairMatch */,
-                        taskConsumer
+                        taskConsumer,
                     )
                     verify(recentsModel).getTasks(capture())
                 }
@@ -567,7 +571,7 @@
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            10 /*alreadyRunningTask*/
+            10, /*alreadyRunningTask*/
         )
         assertTrue(splitSelectStateController.isSplitSelectActive)
     }
@@ -579,21 +583,23 @@
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            -1 /*alreadyRunningTask*/
+            -1, /*alreadyRunningTask*/
         )
         assertTrue(splitSelectStateController.isSplitSelectActive)
     }
 
     @Test
     fun resetAfterInitial() {
+        whenever(context.getOverviewPanel<RecentsView<*, *>>()).thenReturn(recentsView)
         splitSelectStateController.setInitialTaskSelect(
             Intent() /*intent*/,
             -1 /*stagePosition*/,
             ItemInfo(),
             null /*splitEvent*/,
-            -1
+            -1,
         )
         splitSelectStateController.resetState()
+        verify(recentsView, times(1)).resetDesktopTaskFromSplitSelectState()
         assertFalse(splitSelectStateController.isSplitSelectActive)
     }
 
@@ -622,7 +628,7 @@
     // Generate GroupTask with default userId.
     private fun generateGroupTask(
         task1ComponentName: ComponentName,
-        task2ComponentName: ComponentName
+        task2ComponentName: ComponentName,
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
@@ -642,7 +648,7 @@
         return GroupTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50)
+            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
         )
     }
 
@@ -651,7 +657,7 @@
         task1ComponentName: ComponentName,
         userHandle1: UserHandle,
         task2ComponentName: ComponentName,
-        userHandle2: UserHandle
+        userHandle2: UserHandle,
     ): GroupTask {
         val task1 = Task()
         var taskInfo = ActivityManager.RunningTaskInfo()
@@ -674,7 +680,7 @@
         return GroupTask(
             task1,
             task2,
-            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50)
+            SplitConfigurationOptions.SplitBounds(Rect(), Rect(), -1, -1, SNAP_TO_2_50_50),
         )
     }
 }
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index aee49be..f7b04a3 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -31,8 +31,7 @@
     <string name="recent_task_option_split_screen" msgid="6690461455618725183">"Split screen"</string>
     <string name="split_app_info_accessibility" msgid="5475288491241414932">"App info for %1$s"</string>
     <string name="split_app_usage_settings" msgid="7214375263347964093">"Usage settings for %1$s"</string>
-    <!-- no translation found for new_window_option_taskbar (6448780542727767211) -->
-    <skip />
+    <string name="new_window_option_taskbar" msgid="6448780542727767211">"New window"</string>
     <string name="save_app_pair" msgid="5647523853662686243">"Save app pair"</string>
     <string name="app_pair_default_title" msgid="4045241727446873529">"<xliff:g id="APP1">%1$s</xliff:g> | <xliff:g id="APP2">%2$s</xliff:g>"</string>
     <string name="app_pair_unlaunchable_at_screen_size" msgid="3446551575502685376">"This app pair isn\'t supported on this device"</string>
diff --git a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
index 0a50e8b..9f6b40b 100644
--- a/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
+++ b/src/com/android/launcher3/dagger/LauncherBaseAppComponent.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 
+import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.util.DaggerSingletonTracker;
 
 import dagger.BindsInstance;
@@ -32,6 +33,8 @@
  */
 public interface LauncherBaseAppComponent {
     DaggerSingletonTracker getDaggerSingletonTracker();
+    InstallSessionHelper getInstallSessionHelper();
+
     /** Builder for LauncherBaseAppComponent. */
     interface Builder {
         @BindsInstance Builder appContext(@ApplicationContext Context context);
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 124907f..f36f595 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -31,12 +31,17 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.SessionCommitReceiver;
+import com.android.launcher3.dagger.ApplicationContext;
+import com.android.launcher3.dagger.LauncherAppSingleton;
+import com.android.launcher3.dagger.LauncherBaseAppComponent;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.ItemInstallQueue;
 import com.android.launcher3.util.ApplicationInfoWrapper;
+import com.android.launcher3.util.DaggerSingletonObject;
+import com.android.launcher3.util.DaggerSingletonTracker;
+import com.android.launcher3.util.ExecutorUtil;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SafeCloseable;
@@ -47,10 +52,13 @@
 import java.util.List;
 import java.util.Objects;
 
+import javax.inject.Inject;
+
 /**
  * Utility class to tracking install sessions
  */
 @SuppressWarnings("NewApi")
+@LauncherAppSingleton
 public class InstallSessionHelper implements SafeCloseable {
 
     @NonNull
@@ -64,8 +72,8 @@
     private static final boolean DEBUG = false;
 
     @NonNull
-    public static final MainThreadInitializedObject<InstallSessionHelper> INSTANCE =
-            new MainThreadInitializedObject<>(InstallSessionHelper::new);
+    public static final DaggerSingletonObject<InstallSessionHelper> INSTANCE =
+            new DaggerSingletonObject<>(LauncherBaseAppComponent::getInstallSessionHelper);
 
     @Nullable
     private final LauncherApps mLauncherApps;
@@ -82,10 +90,13 @@
     @Nullable
     private IntSet mPromiseIconIds;
 
-    public InstallSessionHelper(@NonNull final Context context) {
+    @Inject
+    public InstallSessionHelper(@NonNull @ApplicationContext final Context context,
+            DaggerSingletonTracker tracker) {
         mInstaller = context.getPackageManager().getPackageInstaller();
         mAppContext = context.getApplicationContext();
         mLauncherApps = context.getSystemService(LauncherApps.class);
+        ExecutorUtil.executeSyncOnMainOrFail(() -> tracker.addCloseable(this));
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
index ca2ef42..a991981 100644
--- a/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
+++ b/tests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -28,6 +28,7 @@
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
 import com.android.launcher3.util.IntArray
 import com.android.launcher3.util.LauncherModelHelper
@@ -53,6 +54,7 @@
     private val expectedAppPackage = "expectedAppPackage"
     private val expectedInstallerPackage = "expectedInstallerPackage"
     private val mockPackageInstaller: PackageInstaller = mock()
+    private val mTracker: DaggerSingletonTracker = mock()
 
     private lateinit var installSessionHelper: InstallSessionHelper
     private lateinit var launcherApps: LauncherApps
@@ -62,7 +64,7 @@
         whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
         whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
         launcherApps = sandboxContext.spyService(LauncherApps::class.java)
-        installSessionHelper = InstallSessionHelper(sandboxContext)
+        installSessionHelper = InstallSessionHelper(sandboxContext, mTracker)
     }
 
     @Test
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 68004bb..cee88ac 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -26,6 +26,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.app.ActivityManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -103,6 +104,8 @@
     public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
     private static final String TAG = "AbstractLauncherUiTest";
 
+    private static final long BYTES_PER_MEGABYTE = 1 << 20;
+
     private static boolean sDumpWasGenerated = false;
     private static boolean sActivityLeakReported = false;
     private static boolean sSeenKeyguard = false;
@@ -124,6 +127,10 @@
     protected String mTargetPackage;
     private int mLauncherPid;
 
+    private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
+    private final ActivityManager mActivityManager;
+    private long mMemoryBefore;
+
     /** Detects activity leaks and throws an exception if a leak is found. */
     public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
         checkDetectedLeaks(launcher, false);
@@ -192,6 +199,8 @@
     }
 
     protected AbstractLauncherUiTest() {
+        mActivityManager = InstrumentationRegistry.getContext()
+                .getSystemService(ActivityManager.class);
         mLauncher.enableCheckEventsForSuccessfulGestures();
         mLauncher.setAnomalyChecker(AbstractLauncherUiTest::verifyKeyguardInvisible);
         try {
@@ -311,6 +320,26 @@
         initialize(this);
     }
 
+    private long getAvailableMemory() {
+        mActivityManager.getMemoryInfo(mMemoryInfo);
+
+        return Math.divideExact(mMemoryInfo.availMem,  BYTES_PER_MEGABYTE);
+    }
+
+    @Before
+    public void saveMemoryBefore() {
+        mMemoryBefore = getAvailableMemory();
+    }
+
+    @After
+    public void logMemoryAfter() {
+        long memoryAfter = getAvailableMemory();
+
+        Log.d(TAG, "Available memory: before=" + mMemoryBefore
+                + "MB, after=" + memoryAfter
+                + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
+    }
+
     /** Method that should be called when a test starts. */
     public static void onTestStart() {
         waitForSetupWizardDismissal();