Update KQS overview launch animation

Updated the KQS overview launch animation to match the spec: open overview and scroll to the focused task in one fluid motion

Flag: LEGACY ENABLE_KEYBOARD_QUICK_SWITCH ENABLED
Fixes: 313608085
Test: Opened KQS from home and overview
Change-Id: I2d617db29ff46e89fab50bb6f8aee900b6fe649f
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
index 5caf004..f15d12b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.R;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
@@ -47,7 +48,8 @@
 public final class KeyboardQuickSwitchController implements
         TaskbarControllers.LoggableTaskbarController {
 
-    static final int MAX_TASKS = 6;
+    @VisibleForTesting
+    public static final int MAX_TASKS = 6;
 
     @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
 
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 60d0e2b..6698600 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,7 +22,9 @@
 import static android.widget.Toast.LENGTH_SHORT;
 
 import static com.android.app.animation.Interpolators.ACCELERATE_DECELERATE;
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.app.animation.Interpolators.DECELERATE;
+import static com.android.app.animation.Interpolators.LINEAR;
 import static com.android.app.animation.Interpolators.OVERSHOOT_1_2;
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
 import static com.android.launcher3.BaseActivity.EVENT_STARTED;
@@ -134,6 +136,7 @@
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TaskViewSimulator;
+import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
@@ -167,6 +170,9 @@
 
     private static final ArrayList<String> STATE_NAMES = new ArrayList<>();
 
+    // Fraction of the scroll and transform animation in which the current task fades out
+    private static final float KQS_TASK_FADE_ANIMATION_FRACTION = 0.4f;
+
     protected final BaseActivityInterface<S, T> mActivityInterface;
     protected final InputConsumerProxy mInputConsumerProxy;
     protected final ActivityInitListener mActivityInitListener;
@@ -900,7 +906,10 @@
             return;
         }
         mLauncherTransitionController.setProgress(
-                Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
+                // Immediately finish the grid transition
+                isKeyboardTaskFocusPending()
+                        ? 1f : Math.max(mCurrentShift.value, getScaleProgressDueToScroll()),
+                mDragLengthFactor);
     }
 
     /**
@@ -1349,7 +1358,9 @@
         }
         Interpolator interpolator;
         S state = mActivityInterface.stateFromGestureEndTarget(endTarget);
-        if (state.displayOverviewTasksAsGrid(mDp)) {
+        if (isKeyboardTaskFocusPending()) {
+            interpolator = EMPHASIZED;
+        } else if (state.displayOverviewTasksAsGrid(mDp)) {
             interpolator = ACCELERATE_DECELERATE;
         } else if (endTarget == RECENTS) {
             interpolator = OVERSHOOT_1_2;
@@ -1653,7 +1664,8 @@
             animatorSet.play(windowAnim);
             if (mRecentsView != null) {
                 mRecentsView.onPrepareGestureEndAnimation(
-                        animatorSet, mGestureState.getEndTarget(),
+                        mGestureState.isHandlingAtomicEvent() ? null : animatorSet,
+                        mGestureState.getEndTarget(),
                         getRemoteTaskViewSimulators());
             }
             animatorSet.setDuration(duration).setInterpolator(interpolator);
@@ -2225,6 +2237,14 @@
         }
     }
 
+    private boolean shouldLinkRecentsViewScroll() {
+        return mRecentsViewScrollLinked && !isKeyboardTaskFocusPending();
+    }
+
+    private boolean isKeyboardTaskFocusPending() {
+        return mRecentsView != null && mRecentsView.isKeyboardTaskFocusPending();
+    }
+
     private void onRecentsViewScroll() {
         if (moveWindowWithRecentsScroll()) {
             onCurrentShiftUpdated();
@@ -2457,6 +2477,44 @@
         mActivityInitListener.register();
     }
 
+    private boolean shouldFadeOutTargetsForKeyboardQuickSwitch(
+            TransformParams transformParams,
+            TaskViewSimulator taskViewSimulator,
+            float progress) {
+        RemoteAnimationTargets targets = transformParams.getTargetSet();
+        boolean fadeAppTargets = isKeyboardTaskFocusPending()
+                && targets != null
+                && targets.apps != null
+                && targets.apps.length > 0;
+        float fadeProgress = Utilities.mapBoundToRange(
+                progress,
+                /* lowerBound= */ 0f,
+                /* upperBound= */ KQS_TASK_FADE_ANIMATION_FRACTION,
+                /* toMin= */ 0f,
+                /* toMax= */ 1f,
+                LINEAR);
+        if (!fadeAppTargets || Float.compare(fadeProgress, 1f) == 0) {
+            return false;
+        }
+        SurfaceTransaction surfaceTransaction =
+                transformParams.createSurfaceParams(taskViewSimulator);
+        SurfaceControl.Transaction transaction = surfaceTransaction.getTransaction();
+
+        for (RemoteAnimationTarget app : targets.apps) {
+            transaction.setAlpha(app.leash, 1f - fadeProgress);
+            transaction.setPosition(app.leash,
+                    /* x= */ app.startBounds.left
+                            + (mActivity.getDeviceProfile().overviewPageSpacing
+                            * (mRecentsView.isRtl() ? fadeProgress : -fadeProgress)),
+                    /* y= */ 0f);
+            transaction.setScale(app.leash, 1f, 1f);
+            taskViewSimulator.taskPrimaryTranslation.value =
+                    mRecentsView.getScrollOffsetForKeyboardTaskFocus();
+            taskViewSimulator.apply(transformParams, surfaceTransaction);
+        }
+        return true;
+    }
+
     /**
      * Applies the transform on the recents animation
      */
@@ -2466,7 +2524,7 @@
         //    swipe-to-icon animation is handled by RectFSpringAnim anim
         boolean notSwipingToHome = mRecentsAnimationTargets != null
                 && mGestureState.getEndTarget() != HOME;
-        boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
+        boolean setRecentsScroll = shouldLinkRecentsViewScroll() && mRecentsView != null;
         float progress = Math.max(mCurrentShift.value, getScaleProgressDueToScroll());
         int scrollOffset = setRecentsScroll ? mRecentsView.getScrollOffset() : 0;
         if (!mStartMovingTasks && (progress > 0 || scrollOffset != 0)) {
@@ -2485,7 +2543,12 @@
                 if (setRecentsScroll) {
                     taskViewSimulator.setScroll(scrollOffset);
                 }
-                taskViewSimulator.apply(remoteHandle.getTransformParams());
+                TransformParams transformParams = remoteHandle.getTransformParams();
+                if (shouldFadeOutTargetsForKeyboardQuickSwitch(
+                        transformParams, taskViewSimulator, progress)) {
+                    continue;
+                }
+                taskViewSimulator.apply(transformParams);
             }
         }
     }
@@ -2493,7 +2556,7 @@
     // Scaling of RecentsView during quick switch based on amount of recents scroll
     private float getScaleProgressDueToScroll() {
         if (mActivity == null || !mActivity.getDeviceProfile().isTablet || mRecentsView == null
-                || !mRecentsViewScrollLinked) {
+                || !shouldLinkRecentsViewScroll()) {
             return 0;
         }
 
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index ca82a2d..b89d20c 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -178,7 +178,7 @@
     public abstract <T extends RecentsView> T getVisibleRecentsView();
 
     @UiThread
-    public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
+    public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
 
     public abstract Rect getOverviewWindowBounds(
             Rect homeBounds, RemoteAnimationTarget target);
@@ -520,7 +520,7 @@
             // Since we are changing the start position of the UI, reapply the state, at the end
             controller.setEndAction(() -> mActivity.getStateManager().goToState(
                     controller.getInterpolatedProgress() > 0.5 ? mTargetState : mBackgroundState,
-                    false));
+                    /* animated= */ false));
 
             RecentsView recentsView = mActivity.getOverviewPanel();
             AnimatorControllerWithResistance controllerWithResistance =
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 693d6ae..27e8726 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -122,7 +122,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         return false;
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 70b9183..97c48e6 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.LauncherState.FLOATING_SEARCH_BAR;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 
@@ -212,7 +211,7 @@
     }
 
     @Override
-    public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
+    public boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener) {
         Launcher launcher = getVisibleLauncher();
         if (launcher == null) {
             return false;
@@ -227,7 +226,7 @@
         closeOverlay();
         launcher.getStateManager().goToState(OVERVIEW,
                 launcher.getStateManager().shouldAnimateStateChange(),
-                onCompleteCallback == null ? null : forEndCallback(onCompleteCallback));
+                animatorListener);
         return true;
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 315316d..e448a14 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -15,9 +15,12 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.PagedView.INVALID_PAGE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Intent;
 import android.graphics.PointF;
 import android.os.SystemClock;
@@ -25,7 +28,6 @@
 import android.view.View;
 
 import androidx.annotation.BinderThread;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 
@@ -75,7 +77,7 @@
      * do not lose the focus across multiple calls of
      * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command
      */
-    private int mTaskFocusIndexOverride = -1;
+    private int mKeyboardTaskFocusIndex = -1;
 
     /**
      * Whether we should incoming toggle commands while a previous toggle command is still ongoing.
@@ -195,9 +197,11 @@
         }
         BaseActivityInterface<?, T> activityInterface =
                 mOverviewComponentObserver.getActivityInterface();
-        RecentsView recents = activityInterface.getVisibleRecentsView();
-        if (recents == null) {
+        RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
+        RecentsView createdRecentsView;
+        if (visibleRecentsView == null) {
             T activity = activityInterface.getCreatedActivity();
+            createdRecentsView = activity == null ? null : activity.getOverviewPanel();
             DeviceProfile dp = activity == null ? null : activity.getDeviceProfile();
             TaskbarUIController uiController = activityInterface.getTaskbarController();
             boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
@@ -209,8 +213,8 @@
                 if (!allowQuickSwitch) {
                     return true;
                 }
-                mTaskFocusIndexOverride = uiController.launchFocusedTask();
-                if (mTaskFocusIndexOverride == -1) {
+                mKeyboardTaskFocusIndex = uiController.launchFocusedTask();
+                if (mKeyboardTaskFocusIndex == -1) {
                     return true;
                 }
             }
@@ -224,34 +228,47 @@
                 return true;
             }
         } else {
+            createdRecentsView = visibleRecentsView;
             switch (cmd.type) {
                 case TYPE_SHOW:
                     // already visible
                     return true;
                 case TYPE_HIDE: {
-                    mTaskFocusIndexOverride = -1;
-                    int currentPage = recents.getNextPage();
-                    TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
-                            ? (TaskView) recents.getPageAt(currentPage)
+                    mKeyboardTaskFocusIndex = INVALID_PAGE;
+                    int currentPage = visibleRecentsView.getNextPage();
+                    TaskView tv = (currentPage >= 0
+                            && currentPage < visibleRecentsView.getTaskViewCount())
+                            ? (TaskView) visibleRecentsView.getPageAt(currentPage)
                             : null;
-                    return launchTask(recents, tv, cmd);
+                    return launchTask(visibleRecentsView, tv, cmd);
                 }
                 case TYPE_TOGGLE:
-                    return launchTask(recents, getNextTask(recents), cmd);
+                    return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd);
                 case TYPE_HOME:
-                    recents.startHome();
+                    visibleRecentsView.startHome();
                     return true;
             }
         }
 
-        final Runnable completeCallback = () -> {
-            RecentsView rv = activityInterface.getVisibleRecentsView();
-            if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-                updateRecentsViewFocus(rv);
+        if (createdRecentsView != null) {
+            createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex);
+        }
+        // Handle recents view focus when launching from home
+        Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() {
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                super.onAnimationStart(animation);
+                updateRecentsViewFocus(cmd);
             }
-            scheduleNextTask(cmd);
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                onRecentsViewFocusUpdated(cmd);
+                scheduleNextTask(cmd);
+            }
         };
-        if (activityInterface.switchToRecentsIfVisible(completeCallback)) {
+        if (activityInterface.switchToRecentsIfVisible(animatorListener)) {
             // If successfully switched, wait until animation finishes
             return false;
         }
@@ -276,6 +293,7 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
+                updateRecentsViewFocus(cmd);
                 activityInterface.runOnInitBackgroundStateUI(() ->
                         interactionHandler.onGestureEnded(0, new PointF()));
                 cmd.removeListener(this);
@@ -290,14 +308,12 @@
                 if (createdActivity == null) {
                     return;
                 }
-                RecentsView createdRecents = createdActivity.getOverviewPanel();
-                if (createdRecents != null) {
-                    createdRecents.onRecentsAnimationComplete();
+                if (createdRecentsView != null) {
+                    createdRecentsView.onRecentsAnimationComplete();
                 }
             }
         };
 
-        RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
         if (visibleRecentsView != null) {
             visibleRecentsView.moveRunningTaskToFront();
         }
@@ -317,7 +333,6 @@
             interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/);
             cmd.mActiveCallbacks.addListener(recentAnimListener);
         }
-
         Trace.beginAsyncSection(TRANSITION_NAME, 0);
         return false;
     }
@@ -325,47 +340,58 @@
     private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
         cmd.removeListener(handler);
         Trace.endAsyncSection(TRANSITION_NAME, 0);
-
-        RecentsView rv =
-                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
-        if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
-            updateRecentsViewFocus(rv);
-        }
+        onRecentsViewFocusUpdated(cmd);
         scheduleNextTask(cmd);
     }
 
-    private void updateRecentsViewFocus(@NonNull RecentsView rv) {
+    private void updateRecentsViewFocus(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE)) {
+            return;
+        }
         // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT),
         // the touch mode somehow is not change to false by the Android framework.
         // The subsequent tab to go through tasks in overview can only be dispatched to
         // focuses views, while focus can only be requested in
         // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
         // here we launch overview with live tile.
-        rv.getViewRootImpl().touchModeChanged(false);
+        recentsView.getViewRootImpl().touchModeChanged(false);
         // Ensure that recents view has focus so that it receives the followup key inputs
-        TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) {
             return;
         }
-        taskView = rv.getNextTaskView();
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getNextTaskView())) {
             return;
         }
-        taskView = rv.getTaskViewAt(0);
-        if (taskView != null) {
-            requestFocus(taskView);
+        if (requestFocus(recentsView.getTaskViewAt(0))) {
             return;
         }
-        requestFocus(rv);
+        requestFocus(recentsView);
     }
 
-    private void requestFocus(@NonNull View view) {
-        view.post(() -> {
-            view.requestFocus();
-            view.requestAccessibilityFocus();
+    private void onRecentsViewFocusUpdated(CommandInfo cmd) {
+        RecentsView recentsView =
+                mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
+        if (recentsView == null
+                || cmd.type != TYPE_HIDE
+                || mKeyboardTaskFocusIndex == INVALID_PAGE) {
+            return;
+        }
+        recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE);
+        recentsView.setCurrentPage(mKeyboardTaskFocusIndex);
+        mKeyboardTaskFocusIndex = INVALID_PAGE;
+    }
+
+    private boolean requestFocus(@Nullable View taskView) {
+        if (taskView == null) {
+            return false;
+        }
+        taskView.post(() -> {
+            taskView.requestFocus();
+            taskView.requestAccessibilityFocus();
         });
+        return true;
     }
 
     public void dump(PrintWriter pw) {
@@ -374,7 +400,7 @@
         if (!mPendingCommands.isEmpty()) {
             pw.println("    pendingCommandType=" + mPendingCommands.get(0).type);
         }
-        pw.println("  mTaskFocusIndexOverride=" + mTaskFocusIndexOverride);
+        pw.println("  mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex);
         pw.println("  mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete);
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 065a9c5..0bb6b23 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -40,6 +40,7 @@
 import android.view.RemoteAnimationTarget;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
@@ -344,6 +345,14 @@
      * Applies the target to the previously set parameters
      */
     public void apply(TransformParams params) {
+        apply(params, null);
+    }
+
+    /**
+     * Applies the target to the previously set parameters, optionally with an overridden
+     * surface transaction
+     */
+    public void apply(TransformParams params, @Nullable SurfaceTransaction surfaceTransaction) {
         if (mDp == null || mThumbnailPosition.isEmpty()) {
             return;
         }
@@ -404,7 +413,8 @@
         mTempRectF.roundOut(mTmpCropRect);
 
         params.setProgress(1f - fullScreenProgress);
-        params.applySurfaceParams(params.createSurfaceParams(this));
+        params.applySurfaceParams(surfaceTransaction == null
+                ? params.createSurfaceParams(this) : surfaceTransaction);
 
         if (!DEBUG) {
             return;
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 66d651e..d10541a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -542,6 +542,9 @@
     private int mOverScrollShift = 0;
     private long mScrollLastHapticTimestamp;
 
+    private int mKeyboardTaskFocusSnapAnimationDuration;
+    private int mKeyboardTaskFocusIndex = INVALID_PAGE;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -1704,6 +1707,7 @@
         // Removing views sets the currentPage to 0, so we save this and restore it after
         // the new set of views are added
         int previousCurrentPage = mCurrentPage;
+        int previousFocusedPage = indexOfChild(getFocusedChild());
         removeAllViews();
 
         // If we are entering Overview as a result of initiating a split from somewhere else
@@ -1833,6 +1837,8 @@
                     targetPage = indexOfChild(currentTaskView);
                 }
             }
+        } else if (previousFocusedPage != INVALID_PAGE) {
+            targetPage = previousFocusedPage;
         } else {
             // Set the current page to the running task, but not if settling on new task.
             if (hasAnyValidTaskIds(runningTaskId)) {
@@ -2914,7 +2920,7 @@
         int focusedTaskShift = 0;
         int focusedTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
-        int snappedPage = getNextPage();
+        int snappedPage = isKeyboardTaskFocusPending() ? mKeyboardTaskFocusIndex : getNextPage();
         TaskView snappedTaskView = getTaskViewAt(snappedPage);
         TaskView homeTaskView = getHomeTaskView();
         TaskView nextFocusedTaskView = null;
@@ -5577,6 +5583,19 @@
     }
 
     /**
+     * Returns how many pixels the running task is offset on the currently laid out dominant axis
+     * specifically during a Keyboard task focus.
+     */
+    public int getScrollOffsetForKeyboardTaskFocus() {
+        if (!isKeyboardTaskFocusPending()) {
+            return getScrollOffset(getRunningTaskIndex());
+        }
+        return getPagedOrientationHandler().getPrimaryScroll(this)
+                - getScrollForPage(mKeyboardTaskFocusIndex)
+                + getScrollOffset(getRunningTaskIndex());
+    }
+
+    /**
      * Sets whether or not we should clamp the scroll offset.
      * This is used to avoid x-axis movement when swiping up transient taskbar.
      * Should only be set at the beginning and end of the gesture, otherwise a jump may occur.
@@ -5609,14 +5628,14 @@
         if (pageIndex == -1) {
             return 0;
         }
-
-        int overScrollShift = getOverScrollShift();
-        if (mAdjacentPageHorizontalOffset > 0) {
-            // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so
-            // that the page can move freely given there's no visual indication why it shouldn't.
-            overScrollShift = (int) Utilities.mapRange(mAdjacentPageHorizontalOffset,
-                    overScrollShift, getUndampedOverScrollShift());
-        }
+        // Don't dampen the scroll (due to overscroll) if the adjacent tasks are offscreen, so that
+        // the page can move freely given there's no visual indication why it shouldn't.
+        int overScrollShift = mAdjacentPageHorizontalOffset > 0
+                        ? (int) Utilities.mapRange(
+                                mAdjacentPageHorizontalOffset,
+                                getOverScrollShift(),
+                                getUndampedOverScrollShift())
+                        : getOverScrollShift();
         return getScrollForPage(pageIndex) - getPagedOrientationHandler().getPrimaryScroll(this)
                 + overScrollShift + getOffsetFromScrollPosition(pageIndex);
     }
@@ -6025,11 +6044,50 @@
         dispatchScrollChanged();
     }
 
+    /**
+     * Prepares this RecentsView to scroll properly for an upcoming child view focus request from
+     * keyboard quick switching
+     */
+    public void setKeyboardTaskFocusIndex(int taskIndex) {
+        mKeyboardTaskFocusIndex = taskIndex;
+    }
+
+    /** Returns whether this RecentsView will be scrolling to a child view for a focus request */
+    public boolean isKeyboardTaskFocusPending() {
+        return mKeyboardTaskFocusIndex != INVALID_PAGE;
+    }
+
+    private boolean isKeyboardTaskFocusPendingForChild(View child) {
+        return isKeyboardTaskFocusPending() && mKeyboardTaskFocusIndex == indexOfChild(child);
+    }
+
     @Override
-    protected boolean shouldHandleRequestChildFocus() {
-        // If we are already scrolling to a task view, then the focus request has already been
-        // handled
-        return mScroller.isFinished();
+    protected int getSnapAnimationDuration() {
+        return isKeyboardTaskFocusPending()
+                ? mKeyboardTaskFocusSnapAnimationDuration : super.getSnapAnimationDuration();
+    }
+
+    @Override
+    protected void onVelocityValuesUpdated() {
+        super.onVelocityValuesUpdated();
+        mKeyboardTaskFocusSnapAnimationDuration =
+                getResources().getInteger(R.integer.config_keyboardTaskFocusSnapAnimationDuration);
+    }
+
+    @Override
+    protected boolean shouldHandleRequestChildFocus(View child) {
+        // If we are already scrolling to a task view and we aren't focusing to this child from
+        // keyboard quick switch, then the focus request has already been handled
+        return mScroller.isFinished() || isKeyboardTaskFocusPendingForChild(child);
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (isKeyboardTaskFocusPendingForChild(child)) {
+            updateGridProperties();
+            updateScrollSynchronously();
+        }
+        super.requestChildFocus(child, focused);
     }
 
     private void dispatchScrollChanged() {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 7191f70..36c591e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -22,6 +22,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.tapl.KeyboardQuickSwitch;
+import com.android.launcher3.taskbar.KeyboardQuickSwitchController;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 
 import org.junit.Assume;
@@ -49,7 +50,7 @@
         DISMISS(0),
         LAUNCH_LAST_APP(0),
         LAUNCH_SELECTED_APP(1),
-        LAUNCH_OVERVIEW(5);
+        LAUNCH_OVERVIEW(KeyboardQuickSwitchController.MAX_TASKS - 1);
 
         private final int mNumAdditionalRunningTasks;
 
@@ -196,7 +197,9 @@
                 if (!testSurface.mInitialFocusAtZero) {
                     kqs.moveFocusBackward();
                 }
-                kqs.launchFocusedOverviewTask();
+                kqs.launchFocusedOverviewTask()
+                        // Check that the correct task was focused
+                        .launchFocusedTaskByEnterKey(CALCULATOR_APP_PACKAGE);
                 break;
             default:
                 throw new IllegalStateException("Cannot run test case: " + testCase);
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
index e718d9c..bddfcfc 100644
--- a/res/values-sw600dp/config.xml
+++ b/res/values-sw600dp/config.xml
@@ -18,6 +18,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">550</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">400</integer>
+
     <!-- The duration of the Widget picker opening and closing animation -->
     <integer name="config_bottomSheetOpenDuration">500</integer>
     <integer name="config_bottomSheetCloseDuration">500</integer>
diff --git a/res/values/config.xml b/res/values/config.xml
index 29c4e66..1b74238 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -43,6 +43,9 @@
     <!-- The duration of the PagedView page snap animation -->
     <integer name="config_pageSnapAnimationDuration">750</integer>
 
+    <!-- The duration of the PagedView page snap animation -->
+    <integer name="config_keyboardTaskFocusSnapAnimationDuration">750</integer>
+
     <!-- View tag key used to store SpringAnimation data. -->
     <item type="id" name="spring_animation_tag" />
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 43dca5c..ca83245 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -637,6 +637,11 @@
         mMinFlingVelocity = res.getDimensionPixelSize(R.dimen.min_fling_velocity);
         mMinSnapVelocity = res.getDimensionPixelSize(R.dimen.min_page_snap_velocity);
         mPageSnapAnimationDuration = res.getInteger(R.integer.config_pageSnapAnimationDuration);
+        onVelocityValuesUpdated();
+    }
+
+    protected void onVelocityValuesUpdated() {
+        // Overridden in RecentsView
     }
 
     @Override
@@ -1582,7 +1587,7 @@
     @Override
     public void requestChildFocus(View child, View focused) {
         super.requestChildFocus(child, focused);
-        if (!shouldHandleRequestChildFocus()) {
+        if (!shouldHandleRequestChildFocus(child)) {
             return;
         }
         // In case the device is controlled by a controller, mCurrentPage isn't updated properly
@@ -1598,7 +1603,7 @@
         }
     }
 
-    protected boolean shouldHandleRequestChildFocus() {
+    protected boolean shouldHandleRequestChildFocus(View child) {
         return true;
     }
 
@@ -1652,7 +1657,7 @@
     }
 
     protected void snapToDestination() {
-        snapToPage(getDestinationPage(), mPageSnapAnimationDuration);
+        snapToPage(getDestinationPage(), getSnapAnimationDuration());
     }
 
     // We want the duration of the page snap animation to be influenced by the distance that
@@ -1676,7 +1681,7 @@
         if (Math.abs(velocity) < mMinFlingVelocity) {
             // If the velocity is low enough, then treat this more as an automatic page advance
             // as opposed to an apparent physical response to flinging
-            return snapToPage(whichPage, mPageSnapAnimationDuration);
+            return snapToPage(whichPage, getSnapAnimationDuration());
         }
 
         // Here we compute a "distance" that will be used in the computation of the overall
@@ -1698,12 +1703,16 @@
         return snapToPage(whichPage, delta, duration);
     }
 
+    protected int getSnapAnimationDuration() {
+        return mPageSnapAnimationDuration;
+    }
+
     public boolean snapToPage(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration);
+        return snapToPage(whichPage, getSnapAnimationDuration());
     }
 
     public boolean snapToPageImmediately(int whichPage) {
-        return snapToPage(whichPage, mPageSnapAnimationDuration, true);
+        return snapToPage(whichPage, getSnapAnimationDuration(), true);
     }
 
     public boolean snapToPage(int whichPage, int duration) {
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
index 1bc489c..d337b91 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/BaseOverview.java
@@ -19,9 +19,11 @@
 import static android.view.KeyEvent.KEYCODE_ESCAPE;
 
 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
+import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
 
 import android.graphics.Rect;
+import android.view.KeyEvent;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -47,6 +49,10 @@
             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
+    private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ENTER");
+    private static final Pattern EVENT_ENTER_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ENTER");
 
     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
 
@@ -414,6 +420,31 @@
         }
     }
 
+    /**
+     * Presses the enter key to launch the focused task
+     * <p>
+     * If no task is focused, this will fail.
+     */
+    public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_DOWN);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP);
+            mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
+
+            mLauncher.executeAndWaitForLauncherStop(
+                    () -> mLauncher.assertTrue(
+                            "Failed to press enter",
+                            mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)),
+                    "pressing enter");
+            mLauncher.assertAppLaunched(expectedPackageName);
+
+            try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                    "pressed enter")) {
+                return new LaunchedAppState(mLauncher);
+            }
+        }
+    }
+
     private void verifyActionsViewVisibility() {
         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
                 "want to assert overview actions view visibility")) {