Merge "[Toast] Fix ANR when tapping on rich card chip." into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
index 3e1a6ae..a9d50b9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchTaskView.java
@@ -40,6 +40,8 @@
 
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * A view that displays a recent task during a keyboard quick switch.
  */
@@ -96,17 +98,18 @@
         Resources resources = mContext.getResources();
 
         Preconditions.assertNotNull(mContent);
-        mBorderAnimator = new BorderAnimator(
+        mBorderAnimator = BorderAnimator.createScalingBorderAnimator(
                 /* borderRadiusPx= */ resources.getDimensionPixelSize(
                         R.dimen.keyboard_quick_switch_task_view_radius),
-                /* borderColor= */ mBorderColor,
-                /* borderAnimationParams= */ new BorderAnimator.ScalingParams(
-                        /* borderWidthPx= */ resources.getDimensionPixelSize(
+                /* borderWidthPx= */ resources.getDimensionPixelSize(
                                 R.dimen.keyboard_quick_switch_border_width),
-                        /* boundsBuilder= */ bounds -> bounds.set(
-                                0, 0, getWidth(), getHeight()),
-                        /* targetView= */ this,
-                        /* contentView= */ mContent));
+                /* boundsBuilder= */ bounds -> {
+                    bounds.set(0, 0, getWidth(), getHeight());
+                    return Unit.INSTANCE;
+                },
+                /* targetView= */ this,
+                /* contentView= */ mContent,
+                /* borderColor= */ mBorderColor);
     }
 
     @Nullable
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index c54bb7e..5182a32 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -89,7 +89,8 @@
         mAppsModelFlags = flags;
         mPackageUserKeytoUidMap = map;
         if (mAppsView != null) {
-            mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap);
+            mAppsView.getAppsStore().setApps(
+                    mApps, mAppsModelFlags, mPackageUserKeytoUidMap, false);
         }
     }
 
@@ -190,7 +191,7 @@
 
         viewController.show(animate);
         mAppsView = mOverlayContext.getAppsView();
-        mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap);
+        mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap, false);
         mAppsView.getFloatingHeaderView()
                 .findFixedRowByType(PredictionRowView.class)
                 .setPredictedApps(mPredictedApps);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 3429df1..419824a 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -137,6 +137,7 @@
     private ISplitSelectListener mSplitSelectListener;
     private IStartingWindowListener mStartingWindowListener;
     private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
+    private String mLauncherActivityClass;
     private IRecentTasksListener mRecentTasksListener;
     private IUnfoldTransitionListener mUnfoldAnimationListener;
     private IDesktopTaskListener mDesktopTaskListener;
@@ -248,7 +249,8 @@
         registerSplitScreenListener(mSplitScreenListener);
         registerSplitSelectListener(mSplitSelectListener);
         setStartingWindowListener(mStartingWindowListener);
-        setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
+        setLauncherUnlockAnimationController(
+                mLauncherActivityClass, mLauncherUnlockAnimationController);
         new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
         setupTransactionQueue();
         registerRecentTasksListener(mRecentTasksListener);
@@ -1109,11 +1111,11 @@
      * changes).
      */
     public void setLauncherUnlockAnimationController(
-            ILauncherUnlockAnimationController controller) {
+            String activityClass, ILauncherUnlockAnimationController controller) {
         if (mSysuiUnlockAnimationController != null) {
             try {
-                mSysuiUnlockAnimationController.setLauncherUnlockController(controller);
-
+                mSysuiUnlockAnimationController.setLauncherUnlockController(
+                        activityClass, controller);
                 if (controller != null) {
                     controller.dispatchSmartspaceStateToSysui();
                 }
@@ -1121,7 +1123,7 @@
                 Log.w(TAG, "Failed call setLauncherUnlockAnimationController", e);
             }
         }
-
+        mLauncherActivityClass = activityClass;
         mLauncherUnlockAnimationController = controller;
     }
 
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
deleted file mode 100644
index 7563187..0000000
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * Copyright (C) 2023 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.util;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.ColorInt;
-import android.annotation.Nullable;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Px;
-
-import com.android.app.animation.Interpolators;
-import com.android.launcher3.anim.AnimatedFloat;
-import com.android.launcher3.anim.AnimatorListeners;
-
-/**
- * Utility class for drawing a rounded-rect border around a view.
- * <p>
- * To use this class:
- * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
- *      provided border bounds. See {@link SimpleParams} and {@link ScalingParams} to determine
- *      which would be best for your target view.
- * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call
- *      {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}.
- * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call
- *      {@link BorderAnimator#setBorderVisible(boolean)} where appropriate.
- */
-public final class BorderAnimator {
-
-    public static final int DEFAULT_BORDER_COLOR = Color.WHITE;
-
-    private static final long DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300;
-    private static final long DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133;
-    private static final Interpolator DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE;
-
-    @NonNull private final AnimatedFloat mBorderAnimationProgress = new AnimatedFloat(
-            this::updateOutline);
-    @Px private final int mBorderRadiusPx;
-    @NonNull private final BorderAnimationParams mBorderAnimationParams;
-    private final long mAppearanceDurationMs;
-    private final long mDisappearanceDurationMs;
-    @NonNull private final Interpolator mInterpolator;
-    @NonNull private final Paint mBorderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
-    @Nullable private Animator mRunningBorderAnimation;
-
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams) {
-        this(borderRadiusPx,
-                borderColor,
-                borderAnimationParams,
-                DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
-                DEFAULT_INTERPOLATOR);
-    }
-
-    /**
-     * @param borderRadiusPx the radius of the border's corners, in pixels
-     * @param borderColor the border's color
-     * @param borderAnimationParams params for handling different target view layout situation.
-     * @param appearanceDurationMs appearance animation duration, in milliseconds
-     * @param disappearanceDurationMs disappearance animation duration, in milliseconds
-     * @param interpolator animation interpolator
-     */
-    public BorderAnimator(
-            @Px int borderRadiusPx,
-            @ColorInt int borderColor,
-            @NonNull BorderAnimationParams borderAnimationParams,
-            long appearanceDurationMs,
-            long disappearanceDurationMs,
-            @NonNull Interpolator interpolator) {
-        mBorderRadiusPx = borderRadiusPx;
-        mBorderAnimationParams = borderAnimationParams;
-        mAppearanceDurationMs = appearanceDurationMs;
-        mDisappearanceDurationMs = disappearanceDurationMs;
-        mInterpolator = interpolator;
-
-        mBorderPaint.setColor(borderColor);
-        mBorderPaint.setStyle(Paint.Style.STROKE);
-        mBorderPaint.setAlpha(0);
-    }
-
-    private void updateOutline() {
-        float interpolatedProgress = mInterpolator.getInterpolation(
-                mBorderAnimationProgress.value);
-
-        mBorderAnimationParams.setProgress(interpolatedProgress);
-        mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress));
-        mBorderPaint.setStrokeWidth(mBorderAnimationParams.getBorderWidth());
-        mBorderAnimationParams.mTargetView.invalidate();
-    }
-
-    /**
-     * Draws the border on the given canvas.
-     * <p>
-     * Call this method in the target view's {@link android.view.View#draw(Canvas)} method after
-     * calling super.
-     */
-    public void drawBorder(Canvas canvas) {
-        float alignmentAdjustment = mBorderAnimationParams.getAlignmentAdjustment();
-        canvas.drawRoundRect(
-                /* left= */ mBorderAnimationParams.mBorderBounds.left + alignmentAdjustment,
-                /* top= */ mBorderAnimationParams.mBorderBounds.top + alignmentAdjustment,
-                /* right= */ mBorderAnimationParams.mBorderBounds.right - alignmentAdjustment,
-                /* bottom= */ mBorderAnimationParams.mBorderBounds.bottom - alignmentAdjustment,
-                /* rx= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* ry= */ mBorderRadiusPx + mBorderAnimationParams.getRadiusAdjustment(),
-                /* paint= */ mBorderPaint);
-    }
-
-    /**
-     * Builds the border appearance/disappearance animation.
-     */
-    @NonNull
-    public Animator buildAnimator(boolean isAppearing) {
-        mRunningBorderAnimation = mBorderAnimationProgress.animateToValue(isAppearing ? 1f : 0f);
-        mRunningBorderAnimation.setDuration(
-                isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs);
-
-        mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mBorderAnimationParams.onShowBorder();
-            }
-        });
-        mRunningBorderAnimation.addListener(
-                AnimatorListeners.forEndCallback(() -> {
-                    mRunningBorderAnimation = null;
-                    if (isAppearing) {
-                        return;
-                    }
-                    mBorderAnimationParams.onHideBorder();
-                }));
-
-        return mRunningBorderAnimation;
-    }
-
-    /**
-     * Immediately shows/hides the border without an animation.
-     * <p>
-     * To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)}
-     */
-    public void setBorderVisible(boolean visible) {
-        if (mRunningBorderAnimation != null) {
-            mRunningBorderAnimation.end();
-        }
-        if (visible) {
-            mBorderAnimationParams.onShowBorder();
-        }
-        mBorderAnimationProgress.updateValue(visible ? 1f : 0f);
-        if (!visible) {
-            mBorderAnimationParams.onHideBorder();
-        }
-    }
-
-    /**
-     * Callback to update the border bounds when building this animation.
-     */
-    public interface BorderBoundsBuilder {
-
-        /**
-         * Sets the given rect to the most up-to-date bounds.
-         */
-        void updateBorderBounds(Rect rect);
-    }
-
-    /**
-     * Params for handling different target view layout situation.
-     */
-    private abstract static class BorderAnimationParams {
-
-        @NonNull private final Rect mBorderBounds = new Rect();
-        @NonNull private final BorderBoundsBuilder mBoundsBuilder;
-
-        @NonNull final View mTargetView;
-        @Px final int mBorderWidthPx;
-
-        private float mAnimationProgress = 0f;
-        @Nullable private View.OnLayoutChangeListener mLayoutChangeListener;
-
-        /**
-         * @param borderWidthPx the width of the border, in pixels
-         * @param boundsBuilder callback to update the border bounds
-         * @param targetView the view that will be drawing the border
-         */
-        private BorderAnimationParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            mBorderWidthPx = borderWidthPx;
-            mBoundsBuilder = boundsBuilder;
-            mTargetView = targetView;
-        }
-
-        private void setProgress(float progress) {
-            mAnimationProgress = progress;
-        }
-
-        private float getBorderWidth() {
-            return mBorderWidthPx * mAnimationProgress;
-        }
-
-        float getAlignmentAdjustment() {
-            // Outset the border by half the width to create an outwards-growth animation
-            return (-getBorderWidth() / 2f) + getAlignmentAdjustmentInset();
-        }
-
-
-        void onShowBorder() {
-            if (mLayoutChangeListener == null) {
-                mLayoutChangeListener =
-                        (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                            onShowBorder();
-                            mTargetView.invalidate();
-                        };
-                mTargetView.addOnLayoutChangeListener(mLayoutChangeListener);
-            }
-            mBoundsBuilder.updateBorderBounds(mBorderBounds);
-        }
-
-        void onHideBorder() {
-            if (mLayoutChangeListener != null) {
-                mTargetView.removeOnLayoutChangeListener(mLayoutChangeListener);
-                mLayoutChangeListener = null;
-            }
-        }
-
-        abstract int getAlignmentAdjustmentInset();
-
-        abstract float getRadiusAdjustment();
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border can be drawn outside the
-     * target view's bounds without any additional logic.
-     */
-    public static final class SimpleParams extends BorderAnimationParams {
-
-        public SimpleParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            return 0;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            return -getAlignmentAdjustment();
-        }
-    }
-
-    /**
-     * Use an instance of this {@link BorderAnimationParams} if the border would other be clipped by
-     * the target view's bound.
-     * <p>
-     * Note: using these params will set the scales and pivots of the
-     * container and content views, however will only reset the scales back to 1.
-     */
-    public static final class ScalingParams extends BorderAnimationParams {
-
-        @NonNull private final View mContentView;
-
-        /**
-         * @param targetView the view that will be drawing the border. this view will be scaled up
-         *                   to make room for the border
-         * @param contentView the view around which the border will be drawn. this view will be
-         *                    scaled down reciprocally to keep its original size and location.
-         */
-        public ScalingParams(
-                @Px int borderWidthPx,
-                @NonNull BorderBoundsBuilder boundsBuilder,
-                @NonNull View targetView,
-                @NonNull View contentView) {
-            super(borderWidthPx, boundsBuilder, targetView);
-            mContentView = contentView;
-        }
-
-        @Override
-        void onShowBorder() {
-            super.onShowBorder();
-            float width = mTargetView.getWidth();
-            float height = mTargetView.getHeight();
-            // Scale up just enough to make room for the border. Fail fast and fix the scaling
-            // onLayout.
-            float scaleX = width == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / width);
-            float scaleY = height == 0 ? 1f : 1f + ((2 * mBorderWidthPx) / height);
-
-            mTargetView.setPivotX(width / 2);
-            mTargetView.setPivotY(height / 2);
-            mTargetView.setScaleX(scaleX);
-            mTargetView.setScaleY(scaleY);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f / scaleX);
-            mContentView.setScaleY(1f / scaleY);
-        }
-
-        @Override
-        void onHideBorder() {
-            super.onHideBorder();
-            mTargetView.setPivotX(mTargetView.getWidth());
-            mTargetView.setPivotY(mTargetView.getHeight());
-            mTargetView.setScaleX(1f);
-            mTargetView.setScaleY(1f);
-
-            mContentView.setPivotX(mContentView.getWidth() / 2f);
-            mContentView.setPivotY(mContentView.getHeight() / 2f);
-            mContentView.setScaleX(1f);
-            mContentView.setScaleY(1f);
-        }
-
-        @Override
-        int getAlignmentAdjustmentInset() {
-            // Inset the border since we are scaling the container up
-            return mBorderWidthPx;
-        }
-
-        @Override
-        float getRadiusAdjustment() {
-            // Increase the radius since we are scaling the container up
-            return getAlignmentAdjustment();
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
new file mode 100644
index 0000000..44eb070
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2023 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.util
+
+import android.animation.Animator
+import android.annotation.ColorInt
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.animation.Interpolator
+import androidx.annotation.Px
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import com.android.app.animation.Interpolators
+import com.android.launcher3.anim.AnimatedFloat
+import kotlin.math.roundToInt
+
+/**
+ * Utility class for drawing a rounded-rect border around a view.
+ *
+ * To use this class:
+ * 1. Create an instance in the target view. NOTE: The border will animate outwards from the
+ *    provided border bounds.
+ * 2. Override the target view's [View.draw] method and call [drawBorder] after
+ *    `super.draw(canvas)`.
+ * 3. Call [buildAnimator] and start the animation or call [setBorderVisibility] where appropriate.
+ */
+class BorderAnimator
+private constructor(
+    @field:Px @param:Px private val borderRadiusPx: Int,
+    @ColorInt borderColor: Int,
+    private val borderAnimationParams: BorderAnimationParams,
+    private val appearanceDurationMs: Long,
+    private val disappearanceDurationMs: Long,
+    private val interpolator: Interpolator,
+) {
+    private val borderAnimationProgress = AnimatedFloat { updateOutline() }
+    private val borderPaint =
+        Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            color = borderColor
+            style = Paint.Style.STROKE
+            alpha = 0
+        }
+    private var runningBorderAnimation: Animator? = null
+
+    companion object {
+        const val DEFAULT_BORDER_COLOR = Color.WHITE
+        private const val DEFAULT_APPEARANCE_ANIMATION_DURATION_MS = 300L
+        private const val DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS = 133L
+        private val DEFAULT_INTERPOLATOR = Interpolators.EMPHASIZED_DECELERATE
+
+        /**
+         * Creates a BorderAnimator that simply draws the border outside the bound of the target
+         * view.
+         *
+         * Use this method if the border can be drawn outside the target view's bounds without any
+         * additional logic.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createSimpleBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                SimpleParams(borderWidthPx, boundsBuilder, targetView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+
+        /**
+         * Creates a BorderAnimator that scales the target and content views to draw the border
+         * within the target's bounds without obscuring the content.
+         *
+         * Use this method if the border would otherwise be clipped by the target view's bound.
+         *
+         * Note: using this method will set the scales and pivots of the container and content
+         * views, however will only reset the scales back to 1.
+         *
+         * @param borderRadiusPx the radius of the border's corners, in pixels
+         * @param borderWidthPx the width of the border, in pixels
+         * @param boundsBuilder callback to update the border bounds
+         * @param targetView the view that will be drawing the border
+         * @param contentView the view around which the border will be drawn. this view will be
+         *   scaled down reciprocally to keep its original size and location.
+         * @param borderColor the border's color
+         * @param appearanceDurationMs appearance animation duration, in milliseconds
+         * @param disappearanceDurationMs disappearance animation duration, in milliseconds
+         * @param interpolator animation interpolator
+         */
+        @JvmOverloads
+        @JvmStatic
+        fun createScalingBorderAnimator(
+            @Px borderRadiusPx: Int,
+            @Px borderWidthPx: Int,
+            boundsBuilder: (rect: Rect?) -> Unit,
+            targetView: View,
+            contentView: View,
+            @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR,
+            appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS,
+            disappearanceDurationMs: Long = DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS,
+            interpolator: Interpolator = DEFAULT_INTERPOLATOR,
+        ): BorderAnimator {
+            return BorderAnimator(
+                borderRadiusPx,
+                borderColor,
+                ScalingParams(borderWidthPx, boundsBuilder, targetView, contentView),
+                appearanceDurationMs,
+                disappearanceDurationMs,
+                interpolator,
+            )
+        }
+    }
+
+    private fun updateOutline() {
+        val interpolatedProgress = interpolator.getInterpolation(borderAnimationProgress.value)
+        borderAnimationParams.animationProgress = interpolatedProgress
+        borderPaint.alpha = (255 * interpolatedProgress).roundToInt()
+        borderPaint.strokeWidth = borderAnimationParams.borderWidth
+        borderAnimationParams.targetView.invalidate()
+    }
+
+    /**
+     * Draws the border on the given canvas.
+     *
+     * Call this method in the target view's [View.draw] method after calling super.
+     */
+    fun drawBorder(canvas: Canvas) {
+        with(borderAnimationParams) {
+            val radius = borderRadiusPx + radiusAdjustment
+            canvas.drawRoundRect(
+                /* left= */ borderBounds.left + alignmentAdjustment,
+                /* top= */ borderBounds.top + alignmentAdjustment,
+                /* right= */ borderBounds.right - alignmentAdjustment,
+                /* bottom= */ borderBounds.bottom - alignmentAdjustment,
+                /* rx= */ radius,
+                /* ry= */ radius,
+                /* paint= */ borderPaint
+            )
+        }
+    }
+
+    /** Builds the border appearance/disappearance animation. */
+    fun buildAnimator(isAppearing: Boolean): Animator {
+        return borderAnimationProgress.animateToValue(if (isAppearing) 1f else 0f).apply {
+            duration = if (isAppearing) appearanceDurationMs else disappearanceDurationMs
+            doOnStart {
+                runningBorderAnimation?.cancel()
+                runningBorderAnimation = this
+                borderAnimationParams.onShowBorder()
+            }
+            doOnEnd {
+                runningBorderAnimation = null
+                if (!isAppearing) {
+                    borderAnimationParams.onHideBorder()
+                }
+            }
+        }
+    }
+
+    /** Shows/hides the border, optionally with an animation. */
+    fun setBorderVisibility(visible: Boolean, animated: Boolean) {
+        if (animated) {
+            buildAnimator(visible).start()
+            return
+        }
+        runningBorderAnimation?.end()
+        if (visible) {
+            borderAnimationParams.onShowBorder()
+        }
+        borderAnimationProgress.updateValue(if (visible) 1f else 0f)
+        if (!visible) {
+            borderAnimationParams.onHideBorder()
+        }
+    }
+
+    /** Params for handling different target view layout situations. */
+    private abstract class BorderAnimationParams(
+        @field:Px @param:Px val borderWidthPx: Int,
+        private val boundsBuilder: (rect: Rect) -> Unit,
+        val targetView: View,
+    ) {
+        val borderBounds = Rect()
+        var animationProgress = 0f
+        private var layoutChangeListener: OnLayoutChangeListener? = null
+
+        abstract val alignmentAdjustmentInset: Int
+        abstract val radiusAdjustment: Float
+
+        val borderWidth: Float
+            get() = borderWidthPx * animationProgress
+        val alignmentAdjustment: Float
+            // Outset the border by half the width to create an outwards-growth animation
+            get() = -borderWidth / 2f + alignmentAdjustmentInset
+
+        open fun onShowBorder() {
+            if (layoutChangeListener == null) {
+                layoutChangeListener = OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+                    onShowBorder()
+                    targetView.invalidate()
+                }
+                targetView.addOnLayoutChangeListener(layoutChangeListener)
+            }
+            boundsBuilder(borderBounds)
+        }
+
+        open fun onHideBorder() {
+            if (layoutChangeListener != null) {
+                targetView.removeOnLayoutChangeListener(layoutChangeListener)
+                layoutChangeListener = null
+            }
+        }
+    }
+
+    /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */
+    private class SimpleParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        override val alignmentAdjustmentInset = 0
+        override val radiusAdjustment: Float
+            get() = -alignmentAdjustment
+    }
+
+    /**
+     * BorderAnimationParams that scales the target and content views to draw the border within the
+     * target's bounds without obscuring the content.
+     */
+    private class ScalingParams(
+        @Px borderWidthPx: Int,
+        boundsBuilder: (rect: Rect?) -> Unit,
+        targetView: View,
+        private val contentView: View,
+    ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) {
+        // Inset the border since we are scaling the container up
+        override val alignmentAdjustmentInset = borderWidthPx
+        override val radiusAdjustment: Float
+            // Increase the radius since we are scaling the container up
+            get() = alignmentAdjustment
+
+        override fun onShowBorder() {
+            super.onShowBorder()
+            val tvWidth = targetView.width.toFloat()
+            val tvHeight = targetView.height.toFloat()
+            // Scale up just enough to make room for the border. Fail fast and fix the scaling
+            // onLayout.
+            val newScaleX = if (tvWidth == 0f) 1f else 1f + 2 * borderWidthPx / tvWidth
+            val newScaleY = if (tvHeight == 0f) 1f else 1f + 2 * borderWidthPx / tvHeight
+            with(targetView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = newScaleX
+                scaleY = newScaleY
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f / newScaleX
+                scaleY = 1f / newScaleY
+            }
+        }
+
+        override fun onHideBorder() {
+            super.onHideBorder()
+            with(targetView) {
+                pivotX = width.toFloat()
+                pivotY = height.toFloat()
+                scaleX = 1f
+                scaleY = 1f
+            }
+            with(contentView) {
+                pivotX = width / 2f
+                pivotY = height / 2f
+                scaleX = 1f
+                scaleY = 1f
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 32d6582..8a6c197 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -62,6 +62,8 @@
 import java.util.List;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains all tasks that are part of the desktop.
  */
@@ -142,9 +144,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(),
                 mBackgroundView.getBottom());
+        return Unit.INSTANCE;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 7e58763..2e347ba 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -37,6 +37,8 @@
 import java.util.HashMap;
 import java.util.function.Consumer;
 
+import kotlin.Unit;
+
 /**
  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
  *
@@ -76,10 +78,10 @@
     }
 
     @Override
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         if (mSplitBoundsConfig == null) {
             super.updateBorderBounds(bounds);
-            return;
+            return Unit.INSTANCE;
         }
         bounds.set(
                 Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
@@ -90,6 +92,7 @@
                         mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())),
                 Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()),
                         mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY())));
+        return Unit.INSTANCE;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index d5b43a8..50b38ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -120,6 +120,8 @@
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
+import kotlin.Unit;
+
 /**
  * A task in the Recents view.
  */
@@ -440,48 +442,44 @@
 
         boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
+        boolean cursorHoverStatesEnabled = FeatureFlags.enableCursorHoverStates();
 
-        boolean willDrawBorder =
-                keyboardFocusHighlightEnabled || FeatureFlags.enableCursorHoverStates();
-        setWillNotDraw(!willDrawBorder);
+        setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled);
 
-        if (willDrawBorder) {
-            TypedArray styledAttrs = context.obtainStyledAttributes(
-                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+        TypedArray styledAttrs = context.obtainStyledAttributes(
+                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
 
-            mFocusBorderAnimator = keyboardFocusHighlightEnabled ? new BorderAnimator(
-                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                    /* borderColor= */ styledAttrs.getColor(
-                            R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR),
-                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                            /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                                    R.dimen.keyboard_quick_switch_border_width),
-                            /* boundsBuilder= */ this::updateBorderBounds,
-                            /* targetView= */ this)) : null;
+        mFocusBorderAnimator = keyboardFocusHighlightEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.keyboard_quick_switch_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            mHoverBorderAnimator =
-                    FeatureFlags.enableCursorHoverStates() ? new BorderAnimator(
-                            /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                            /* borderColor= */ styledAttrs.getColor(
-                                    R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR),
-                            /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                                    /* borderWidthPx= */ context.getResources()
-                                            .getDimensionPixelSize(R.dimen.task_hover_border_width),
-                                    /* boundsBuilder= */ this::updateBorderBounds,
-                                    /* targetView= */ this)) : null;
+        mHoverBorderAnimator = cursorHoverStatesEnabled
+                ? BorderAnimator.createSimpleBorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                R.dimen.task_hover_border_width),
+                        /* boundsBuilder= */ this::updateBorderBounds,
+                        /* targetView= */ this,
+                        /* borderColor= */ styledAttrs.getColor(
+                                R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR))
+                : null;
 
-            styledAttrs.recycle();
-        } else {
-            mFocusBorderAnimator = null;
-            mHoverBorderAnimator = null;
-        }
+        styledAttrs.recycle();
     }
 
-    protected void updateBorderBounds(Rect bounds) {
+    protected Unit updateBorderBounds(@NonNull Rect bounds) {
         bounds.set(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
                 mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
                 mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()));
+        return Unit.INSTANCE;
     }
 
     public void setTaskViewId(int id) {
@@ -529,19 +527,21 @@
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         if (mFocusBorderAnimator != null) {
-            mFocusBorderAnimator.buildAnimator(gainFocus).start();
+            mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true);
         }
     }
 
     @Override
     public boolean onHoverEvent(MotionEvent event) {
-        if (FeatureFlags.enableCursorHoverStates()) {
+        if (mHoverBorderAnimator != null) {
             switch (event.getAction()) {
                 case MotionEvent.ACTION_HOVER_ENTER:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ true).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ true, /* animated= */ true);
                     break;
                 case MotionEvent.ACTION_HOVER_EXIT:
-                    mHoverBorderAnimator.buildAnimator(/* isAppearing= */ false).start();
+                    mHoverBorderAnimator.setBorderVisibility(
+                            /* visible= */ false, /* animated= */ true);
                     break;
                 default:
                     break;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
new file mode 100644
index 0000000..74f37a4
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2023 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.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.tapl.KeyboardQuickSwitch;
+import com.android.launcher3.ui.TaplTestsLauncher3;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TaplTestsKeyboardQuickSwitch extends AbstractQuickStepTest {
+
+    private enum TestSurface {
+        HOME, LAUNCHED_APP, HOME_ALL_APPS, WIDGETS,
+    }
+
+    private enum TestCase {
+        DISMISS(0),
+        LAUNCH_LAST_APP(0),
+        LAUNCH_SELECTED_APP(1),
+        LAUNCH_OVERVIEW(5);
+
+        private final int mNumAdditionalRunningTasks;
+
+        TestCase(int numAdditionalRunningTasks) {
+            mNumAdditionalRunningTasks = numAdditionalRunningTasks;
+        }
+    }
+
+    private static final String CALCULATOR_APP_PACKAGE =
+            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
+
+    @Override
+    public void setUp() throws Exception {
+        Assume.assumeTrue(mLauncher.isTablet());
+        super.setUp();
+        TaplTestsLauncher3.initialize(this);
+        startAppFast(CALCULATOR_APP_PACKAGE);
+        startTestActivity(2);
+    }
+
+    @Test
+    public void testDismiss_fromHome() {
+        runTest(TestSurface.HOME, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testDismiss_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.DISMISS);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchLastTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_LAST_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchSelectedTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_SELECTED_APP);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromHome() {
+        runTest(TestSurface.HOME, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromApp() {
+        runTest(TestSurface.LAUNCHED_APP, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromHomeAllApps() {
+        runTest(TestSurface.HOME_ALL_APPS, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    @Test
+    public void testLaunchOverviewTask_fromWidgets() {
+        runTest(TestSurface.WIDGETS, TestCase.LAUNCH_OVERVIEW);
+    }
+
+    private void runTest(@NonNull TestSurface testSurface, @NonNull TestCase testCase) {
+        for (int i = 0; i < testCase.mNumAdditionalRunningTasks; i++) {
+            startTestActivity(3 + i);
+        }
+
+        KeyboardQuickSwitch kqs;
+        switch (testSurface) {
+            case HOME:
+                kqs = mLauncher.goHome().showQuickSwitchView();
+                break;
+            case LAUNCHED_APP:
+                mLauncher.setIgnoreTaskbarVisibility(true);
+                kqs = mLauncher.getLaunchedAppState().showQuickSwitchView();
+                break;
+            case HOME_ALL_APPS:
+                kqs = mLauncher.goHome().switchToAllApps().showQuickSwitchView();
+                break;
+            case WIDGETS:
+                kqs = mLauncher.goHome().openAllWidgets().showQuickSwitchView();
+                break;
+            default:
+                throw new IllegalStateException(
+                        "KeyboardQuickSwitch could not be initialized for test surface: "
+                            + testSurface);
+        }
+
+        switch (testCase) {
+            case DISMISS:
+                kqs.dismiss();
+                break;
+            case LAUNCH_LAST_APP:
+                kqs.launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                break;
+            case LAUNCH_SELECTED_APP:
+                kqs.moveFocusForward().launchFocusedAppTask(CALCULATOR_APP_PACKAGE);
+                break;
+            case LAUNCH_OVERVIEW:
+                kqs.moveFocusBackward().moveFocusBackward().launchFocusedOverviewTask();
+                break;
+            default:
+                throw new IllegalStateException("Cannot run test case: " + testCase);
+        }
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 5531c6e..211a13c 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -17,10 +17,6 @@
 package com.android.quickstep;
 
 import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
-import static com.android.launcher3.testing.shared.TestProtocol.FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP;
-import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
@@ -32,7 +28,6 @@
 
 import android.content.Intent;
 import android.platform.test.annotations.PlatinumTest;
-import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -51,11 +46,9 @@
 import com.android.launcher3.tapl.OverviewTaskMenu;
 import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
 import com.android.launcher3.ui.TaplTestsLauncher3;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 import com.android.quickstep.views.RecentsView;
@@ -320,7 +313,7 @@
     @Test
     @ScreenRecord // b/242163205
     @PlatinumTest(focusArea = "launcher")
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/286084688
+    @TaskbarModeSwitch(mode = PERSISTENT)
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
@@ -340,16 +333,7 @@
                 "The first app we should have quick switched to is not running");
         // Expect task bar visible when the launched app was the test activity.
         launchedAppState = getAndAssertLaunchedApp();
-
-        Log.e(FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP,
-                "is Taskbar Transient : " + DisplayController.isTransientTaskbar(mTargetContext));
-        // TODO(b/286084688): Remove this branching check after test corruption is resolved.
-        // Branching this check because of test corruption.
-        if (DisplayController.isTransientTaskbar(mTargetContext)) {
-            launchedAppState.assertTaskbarHidden();
-        } else {
-            launchedAppState.assertTaskbarVisible();
-        }
+        launchedAppState.assertTaskbarVisible();
     }
 
     @Test
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0cc1887..4215e31 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -43,7 +43,6 @@
 import static com.android.launcher3.LauncherState.NO_SCALE;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.Utilities.postAsyncCallback;
-import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
 import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.config.FeatureFlags.MULTI_SELECT_EDIT_MODE;
 import static com.android.launcher3.config.FeatureFlags.SHOW_DOT_PAGINATION;
@@ -114,7 +113,6 @@
 import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.KeyboardShortcutGroup;
-import android.view.KeyboardShortcutInfo;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MotionEvent;
@@ -137,7 +135,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.accessibility.BaseAccessibilityDelegate.LauncherAction;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsRecyclerView;
@@ -205,6 +202,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.KeyboardShortcutsDelegate;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageUserKey;
@@ -335,6 +333,7 @@
     private static final boolean DESKTOP_MODE_SUPPORTED =
             "1".equals(Utilities.getSystemProperty("persist.wm.debug.desktop_mode_2", "0"));
 
+    KeyboardShortcutsDelegate mKeyboardShortcutsDelegate = new KeyboardShortcutsDelegate(this);
     @Thunk
     Workspace<?> mWorkspace;
     @Thunk
@@ -3004,84 +3003,51 @@
         mOverlayManager.dump(prefix, writer);
     }
 
+    /**
+     * Populates the list of shortcuts. Logic delegated to {@Link KeyboardShortcutsDelegate}.
+     *
+     * @param data The data list to populate with shortcuts.
+     * @param menu The current menu, which may be null.
+     * @param deviceId The id for the connected device the shortcuts should be provided for.
+     */
     @Override
     public void onProvideKeyboardShortcuts(
             List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
-
-        ArrayList<KeyboardShortcutInfo> shortcutInfos = new ArrayList<>();
-        if (isInState(NORMAL)) {
-            shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.all_apps_button_label),
-                    KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON));
-            shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.widget_button_text),
-                    KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON));
-        }
-        getSupportedActions(this,  getCurrentFocus()).forEach(la ->
-                shortcutInfos.add(new KeyboardShortcutInfo(
-                        la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON)));
-        if (!shortcutInfos.isEmpty()) {
-            data.add(new KeyboardShortcutGroup(getString(R.string.home_screen), shortcutInfos));
-        }
-
+        mKeyboardShortcutsDelegate.onProvideKeyboardShortcuts(data, menu, deviceId);
         super.onProvideKeyboardShortcuts(data, menu, deviceId);
     }
 
+    /**
+     * Logic delegated to {@Link KeyboardShortcutsDelegate}.
+     * @param keyCode The value in event.getKeyCode().
+     * @param event Description of the key event.
+     */
     @Override
     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
-        if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
-            switch (keyCode) {
-                case KeyEvent.KEYCODE_A:
-                    if (isInState(NORMAL)) {
-                        getStateManager().goToState(ALL_APPS);
-                        return true;
-                    }
-                    break;
-                case KeyEvent.KEYCODE_W:
-                    if (isInState(NORMAL)) {
-                        OptionsPopupView.openWidgets(this);
-                        return true;
-                    }
-                    break;
-                default:
-                    for (LauncherAction la : getSupportedActions(this, getCurrentFocus())) {
-                        if (la.keyCode == keyCode) {
-                            return la.invokeFromKeyboard(getCurrentFocus());
-                        }
-                    }
-            }
-        }
-        return super.onKeyShortcut(keyCode, event);
+        Boolean result = mKeyboardShortcutsDelegate.onKeyShortcut(keyCode, event);
+        return result != null ? result : super.onKeyShortcut(keyCode, event);
     }
 
+    /**
+     * Logic delegated to {@Link KeyboardShortcutsDelegate}.
+     * @param keyCode The value in event.getKeyCode().
+     * @param event Description of the key event.
+     */
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
-            // Close any open floating views.
-            closeOpenViews();
-            return true;
-        }
-        return super.onKeyDown(keyCode, event);
+        Boolean result = mKeyboardShortcutsDelegate.onKeyDown(keyCode, event);
+        return result != null ? result : super.onKeyDown(keyCode, event);
     }
 
+    /**
+     * Logic delegated to {@Link KeyboardShortcutsDelegate}.
+     * @param keyCode The value in event.getKeyCode().
+     * @param event Description of the key event.
+     */
     @Override
     public boolean onKeyUp(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_MENU) {
-            // KEYCODE_MENU is sent by some tests, for example
-            // LauncherJankTests#testWidgetsContainerFling. Don't just remove its handling.
-            if (!mDragController.isDragging() && !mWorkspace.isSwitchingState() &&
-                    isInState(NORMAL)) {
-                // Close any open floating views.
-                closeOpenViews();
-
-                // Setting the touch point to (-1, -1) will show the options popup in the center of
-                // the screen.
-                if (Utilities.isRunningInTestHarness()) {
-                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on key up");
-                }
-                showDefaultOptions(-1, -1);
-            }
-            return true;
-        }
-        return super.onKeyUp(keyCode, event);
+        Boolean result = mKeyboardShortcutsDelegate.onKeyUp(keyCode, event);
+        return result != null ? result : super.onKeyUp(keyCode, event);
     }
 
     /**
@@ -3147,7 +3113,7 @@
         getStateManager().goToState(LauncherState.NORMAL);
     }
 
-    private void closeOpenViews() {
+    public void closeOpenViews() {
         closeOpenViews(true);
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index e724858..378dbf3 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -82,17 +82,29 @@
     }
 
     /**
+     * Calling {@link #setApps(AppInfo[], int, Map, boolean)} with shouldPreinflate set to
+     * {@code true}. This method should be called in launcher (not for taskbar).
+     */
+    public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
+        setApps(apps, flags, map, /* shouldPreinflate= */ true);
+    }
+
+    /**
      * Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for
      * the current set of apps.
+     *
+     * <p> Note that shouldPreinflate param should be set to {@code false} for taskbar, because this
+     * method is too late to preinflate all apps, as user will open all apps in the same frame.
      */
-    public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map)  {
+    public void setApps(@Nullable AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map,
+            boolean shouldPreinflate)  {
         mApps = apps == null ? EMPTY_ARRAY : apps;
         mModelFlags = flags;
         notifyUpdate();
         mPackageUserKeytoUidMap = map;
         // Preinflate all apps RV when apps has changed, which can happen after unlocking screen,
         // rotating screen, or downloading/upgrading apps.
-        if (ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
+        if (shouldPreinflate && ENABLE_ALL_APPS_RV_PREINFLATION.get()) {
             mAllAppsRecyclerViewPool.preInflateAllAppsViewHolders(mContext);
         }
     }
diff --git a/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java
new file mode 100644
index 0000000..3ec339d
--- /dev/null
+++ b/src/com/android/launcher3/util/KeyboardShortcutsDelegate.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions;
+
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.KeyboardShortcutInfo;
+import android.view.Menu;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
+import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.views.OptionsPopupView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Delegate to define the keyboard shortcuts.
+ */
+public class KeyboardShortcutsDelegate {
+
+    Launcher mLauncher;
+
+    public KeyboardShortcutsDelegate(Launcher launcher) {
+        mLauncher = launcher;
+    }
+
+    /**
+     * Populates the list of shortcuts.
+     */
+    public void onProvideKeyboardShortcuts(
+            List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
+        ArrayList<KeyboardShortcutInfo> shortcutInfos = new ArrayList<>();
+        if (mLauncher.isInState(NORMAL)) {
+            shortcutInfos.add(
+                    new KeyboardShortcutInfo(mLauncher.getString(R.string.all_apps_button_label),
+                            KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON));
+            shortcutInfos.add(
+                    new KeyboardShortcutInfo(mLauncher.getString(R.string.widget_button_text),
+                            KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON));
+        }
+        getSupportedActions(mLauncher, mLauncher.getCurrentFocus()).forEach(la ->
+                shortcutInfos.add(new KeyboardShortcutInfo(
+                        la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON)));
+        if (!shortcutInfos.isEmpty()) {
+            data.add(new KeyboardShortcutGroup(mLauncher.getString(R.string.home_screen),
+                    shortcutInfos));
+        }
+    }
+
+    /**
+     * Handles combinations of keys like ctrl+s or ctrl+c and runs before onKeyDown.
+     * @param keyCode code of the key being pressed.
+     * @see android.view.KeyEvent
+     * @return weather the event is already handled and if it should be passed to other components.
+     */
+    public Boolean onKeyShortcut(int keyCode, KeyEvent event) {
+        if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_A:
+                    if (mLauncher.isInState(NORMAL)) {
+                        mLauncher.getStateManager().goToState(ALL_APPS);
+                        return true;
+                    }
+                    break;
+                case KeyEvent.KEYCODE_W:
+                    if (mLauncher.isInState(NORMAL)) {
+                        OptionsPopupView.openWidgets(mLauncher);
+                        return true;
+                    }
+                    break;
+                default:
+                    for (BaseAccessibilityDelegate.LauncherAction la : getSupportedActions(
+                            mLauncher, mLauncher.getCurrentFocus())) {
+                        if (la.keyCode == keyCode) {
+                            return la.invokeFromKeyboard(mLauncher.getCurrentFocus());
+                        }
+                    }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Handle key down event.
+     * @param keyCode code of the key being pressed.
+     * @see android.view.KeyEvent
+     */
+    public Boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE) {
+            // Close any open floating views.
+            mLauncher.closeOpenViews();
+            return true;
+        }
+        return null;
+    }
+
+    /**
+     * Handle key up event.
+     * @param keyCode code of the key being pressed.
+     * @see android.view.KeyEvent
+     */
+    public Boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_MENU) {
+            // KEYCODE_MENU is sent by some tests, for example
+            // LauncherJankTests#testWidgetsContainerFling. Don't just remove its handling.
+            if (!mLauncher.getDragController().isDragging()
+                    && !mLauncher.getWorkspace().isSwitchingState()
+                    && mLauncher.isInState(NORMAL)) {
+                // Close any open floating views.
+                mLauncher.closeOpenViews();
+
+                // Setting the touch point to (-1, -1) will show the options popup in the center of
+                // the screen.
+                if (Utilities.isRunningInTestHarness()) {
+                    Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Opening options popup on key up");
+                }
+                mLauncher.showDefaultOptions(-1, -1);
+            }
+            return true;
+        }
+        return null;
+    }
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index e1b97de..654edad 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -43,6 +43,7 @@
     name: "launcher-oop-tests-src",
     srcs: [
       "src/com/android/launcher3/allapps/OopTaplOpenCloseAllApps.java",
+      "src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java",
       "src/com/android/launcher3/dragging/TaplDragTest.java",
       "src/com/android/launcher3/dragging/TaplUninstallRemove.java",
       "src/com/android/launcher3/ui/AbstractLauncherUiTest.java",
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 0798e97..f5aa820 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -155,7 +155,6 @@
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
-    public static final String FLAKY_QUICK_SWITCH_TO_PREVIOUS_APP = "b/286084688";
     public static final String ICON_MISSING = "b/282963545";
     public static final String INCORRECT_HOME_STATE = "b/293191790";
 
diff --git a/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
new file mode 100644
index 0000000..66c67f5
--- /dev/null
+++ b/tests/src/com/android/launcher3/appiconmenu/TaplAppIconMenuTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.appiconmenu;
+
+import static com.android.launcher3.ui.TaplTestsLauncher3.APP_NAME;
+import static com.android.launcher3.ui.TaplTestsLauncher3.initialize;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.platform.test.annotations.PlatinumTest;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.popup.ArrowPopup;
+import com.android.launcher3.tapl.AllApps;
+import com.android.launcher3.tapl.AppIconMenu;
+import com.android.launcher3.tapl.AppIconMenuItem;
+import com.android.launcher3.tapl.HomeAllApps;
+import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test run in both Out of process (Oop) and in-process (Ipc).
+ * Tests the AppIconMenu (the menu that appears when you long press an app icon) and also make sure
+ * we can launch a shortcut from it.
+ */
+public class TaplAppIconMenuTest extends AbstractLauncherUiTest {
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        initialize(this);
+    }
+
+    private boolean isOptionsPopupVisible(Launcher launcher) {
+        final ArrowPopup<?> popup = launcher.getOptionsPopup();
+        return popup != null && popup.isShown();
+    }
+
+    /**
+     * Open All apps then open the AppIconMenu then launch a shortcut from the menu and make sure it
+     * launches.
+     */
+    @Test
+    @PortraitLandscape
+    @PlatinumTest(focusArea = "launcher")
+    public void testLaunchMenuItem() {
+        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        allApps.freeze();
+        try {
+            final AppIconMenu menu = allApps.getAppIcon(APP_NAME).openDeepShortcutMenu();
+
+            executeOnLauncher(
+                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                            isOptionsPopupVisible(launcher)));
+
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+
+    /**
+     * Drag icon from AllApps to the workspace and then open the AppIconMenu and launch a shortcut
+     * from it.
+     */
+    @Test
+    public void testLaunchHomeScreenMenuItem() {
+        // Drag the test app icon to home screen and open short cut menu from the icon
+        final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
+        allApps.freeze();
+        try {
+            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
+            final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon(
+                    APP_NAME).openDeepShortcutMenu();
+
+            executeOnLauncher(
+                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
+                            isOptionsPopupVisible(launcher)));
+
+            final AppIconMenuItem menuItem = menu.getMenuItem(1);
+            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
+            menuItem.launch(getAppPackageName());
+        } finally {
+            allApps.unfreeze();
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index e6fdbaa..f0c4fdb 100644
--- a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -323,7 +323,7 @@
         mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
         mBubbleTextView.applyLabel(mItemInfoWithIcon);
         mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
 
         mBubbleTextView.onPreDraw();
 
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 5417e3f..bc53d6d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -36,11 +36,8 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.popup.ArrowPopup;
 import com.android.launcher3.tapl.AllApps;
 import com.android.launcher3.tapl.AppIcon;
-import com.android.launcher3.tapl.AppIconMenu;
-import com.android.launcher3.tapl.AppIconMenuItem;
 import com.android.launcher3.tapl.HomeAllApps;
 import com.android.launcher3.tapl.HomeAppIcon;
 import com.android.launcher3.tapl.Widgets;
@@ -89,7 +86,6 @@
     public static void initialize(
             AbstractLauncherUiTest test, boolean clearWorkspace) throws Exception {
         test.reinitializeLauncherData(clearWorkspace);
-        test.mLauncher.resetFreezeRecentTaskList();
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
         test.waitForState("Launcher internal state didn't switch to Home",
@@ -248,55 +244,6 @@
         return getWidgetsView(launcher).computeVerticalScrollOffset();
     }
 
-    private boolean isOptionsPopupVisible(Launcher launcher) {
-        final ArrowPopup<?> popup = launcher.getOptionsPopup();
-        return popup != null && popup.isShown();
-    }
-
-    @Test
-    @PortraitLandscape
-    @PlatinumTest(focusArea = "launcher")
-    public void testLaunchMenuItem() throws Exception {
-        final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-        try {
-            final AppIconMenu menu = allApps.
-                    getAppIcon(APP_NAME).
-                    openDeepShortcutMenu();
-
-            executeOnLauncher(
-                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
-                            isOptionsPopupVisible(launcher)));
-
-            final AppIconMenuItem menuItem = menu.getMenuItem(1);
-            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
-            menuItem.launch(getAppPackageName());
-        } finally {
-            allApps.unfreeze();
-        }
-    }
-
-    @Test
-    public void testLaunchHomeScreenMenuItem() {
-        // Drag the test app icon to home screen and open short cut menu from the icon
-        final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
-        allApps.freeze();
-        try {
-            allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false);
-            final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon(
-                    APP_NAME).openDeepShortcutMenu();
-
-            executeOnLauncher(
-                    launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
-                            isOptionsPopupVisible(launcher)));
-
-            final AppIconMenuItem menuItem = menu.getMenuItem(1);
-            assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText());
-            menuItem.launch(getAppPackageName());
-        } finally {
-            allApps.unfreeze();
-        }
-    }
     @FlakyTest(bugId = 256615483)
     @Test
     @PortraitLandscape
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 44875d5..b82fa35 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -42,7 +42,8 @@
 /**
  * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview.
  */
-public abstract class AllApps extends LauncherInstrumentation.VisibleContainer {
+public abstract class AllApps extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     // Defer updates flag used to defer all apps updates by a test's request.
     private static final int DEFER_UPDATES_TEST = 1 << 1;
 
@@ -65,6 +66,16 @@
                 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
     private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler,
             BySelector appIconSelector, int displayBottom) {
         final UiObject2 icon;
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 677f204..8713b68 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -39,7 +39,8 @@
  * Indicates the base state with a UI other than Overview running as foreground. It can also
  * indicate Launcher as long as Launcher is not in Overview state.
  */
-public abstract class Background extends LauncherInstrumentation.VisibleContainer {
+public abstract class Background extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     private static final int ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION = 500;
     private static final Pattern SQUARE_BUTTON_EVENT = Pattern.compile("onOverviewToggle");
 
@@ -47,6 +48,16 @@
         super(launcher);
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
     /**
      * Swipes up or presses the square button to switch to Overview.
      * Returns the base overview, which can be either in Launcher or the fallback recents.
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 252435b..85e28e8 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -62,4 +62,9 @@
     protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
         return true;
     }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index 8542f91..d9b179c 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -113,4 +113,9 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getWorkspace();
     }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
new file mode 100644
index 0000000..2a98a24
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitch.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import static com.android.launcher3.tapl.LauncherInstrumentation.KEYBOARD_QUICK_SWITCH_RES_ID;
+
+import android.view.KeyEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.testing.shared.TestProtocol;
+
+import java.util.regex.Pattern;
+
+/**
+ * Operations on the Keyboard Quick Switch View
+ */
+public final class KeyboardQuickSwitch {
+
+    private static final Pattern EVENT_ALT_TAB_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_TAB_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON");
+
+    private static final Pattern EVENT_ALT_SHIFT_TAB_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON|META_SHIFT_ON");
+    private static final Pattern EVENT_ALT_SHIFT_TAB_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_TAB"
+                    + ".*?metaState=META_ALT_ON|META_SHIFT_ON");
+    private static final Pattern EVENT_ALT_ESC_DOWN = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_DOWN"
+                    + ".*?keyCode=KEYCODE_ESCAPE.*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
+            "KeyboardQuickSwitchView key event: KeyEvent.*?action=ACTION_UP"
+                    + ".*?keyCode=KEYCODE_ESCAPE.*?metaState=META_ALT_ON");
+    private static final Pattern EVENT_ALT_LEFT_UP = Pattern.compile(
+            "Key event: KeyEvent.*?action=ACTION_UP"
+                    + ".*?keyCode=KEYCODE_ALT_LEFT");
+
+    private final LauncherInstrumentation mLauncher;
+    private final LauncherInstrumentation.ContainerType mStartingContainerType;
+    private final boolean mExpectHomeKeyEventsOnDismiss;
+
+    KeyboardQuickSwitch(
+            LauncherInstrumentation launcher,
+            LauncherInstrumentation.ContainerType startingContainerType,
+            boolean expectHomeKeyEventsOnDismiss) {
+        mLauncher = launcher;
+        mStartingContainerType = startingContainerType;
+        mExpectHomeKeyEventsOnDismiss = expectHomeKeyEventsOnDismiss;
+    }
+
+    /**
+     * Focuses the next task in the Keyboard quick switch view.
+     * <p>
+     * Tasks are ordered left-to-right in LTR, and vice versa in RLT, in a carousel.
+     * <ul>
+     *      <li>If no task has been focused yet, and there is only one task, then that task will be
+     *          focused</li>
+     *      <li>If no task has been focused yet, and there are two or more tasks, then the second
+     *          task will be focused</li>
+     *      <li>If the currently-focused task is at the end of the list, the first task will be
+     *          focused</li>
+     * </ul>
+     */
+    public KeyboardQuickSwitch moveFocusForward() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to move keyboard quick switch focus forward")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_TAB_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_TAB_UP);
+                mLauncher.assertTrue("Failed to press alt+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_TAB, KeyEvent.META_ALT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+tab")) {
+                    mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+                    return this;
+                }
+            }
+        }
+    }
+
+    /**
+     * Focuses the next task in the Keyboard quick switch view.
+     * <p>
+     * Tasks are ordered left-to-right in LTR, and vice versa in RLT, in a carousel.
+     * <ul>
+     *      <li>If no task has been focused yet, and there is only one task, then that task will be
+     *          focused</li>
+     *      <li>If no task has been focused yet, and there are two or more tasks, then the second
+     *          task will be focused</li>
+     *      <li>If the currently-focused task is at the start of the list, the last task will be
+     *          focused</li>
+     * </ul>
+     */
+    public KeyboardQuickSwitch moveFocusBackward() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to move keyboard quick switch focus backward")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_SHIFT_TAB_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_SHIFT_TAB_UP);
+                mLauncher.assertTrue("Failed to press alt+shift+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_TAB,
+                                KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+shift+tab")) {
+                    mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+                    return this;
+                }
+            }
+        }
+    }
+
+    /**
+     * Dismisses the Keyboard Quick Switch view without launching the focused task.
+     * <p>
+     * The device will return to the same state it started in before displaying the Keyboard Quick
+     * Switch view.
+     */
+    public void dismiss() {
+        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                "want to dismiss keyboard quick switch view")) {
+            mLauncher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_DOWN);
+                mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
+                mLauncher.assertTrue("Failed to press alt+tab",
+                        mLauncher.getDevice().pressKeyCode(
+                                KeyEvent.KEYCODE_ESCAPE, KeyEvent.META_ALT_ON));
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                        "pressed alt+esc")) {
+                    mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+                    if (mExpectHomeKeyEventsOnDismiss) {
+                        mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_LEFT_UP);
+                    }
+                    mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+
+                    // Verify the final state is the same as the initial state
+                    mLauncher.verifyContainerType(mStartingContainerType);
+                }
+            }
+        }
+    }
+
+    /**
+     * Launches the currently-focused app task.
+     * <p>
+     * This method should only be used if the focused task is for a recent running app, otherwise
+     * use {@link #launchFocusedOverviewTask()}.
+     *
+     * @param expectedPackageName the package name of the expected launched app
+     */
+    public LaunchedAppState launchFocusedAppTask(@NonNull String expectedPackageName) {
+        return (LaunchedAppState) launchFocusedTask(expectedPackageName);
+    }
+
+    /**
+     * Launches the currently-focused overview task.
+     * <p>
+     * This method only should be used if the focused task is for overview, otherwise use
+     * {@link #launchFocusedAppTask(String)}.
+     */
+    public Overview launchFocusedOverviewTask() {
+        return (Overview) launchFocusedTask(null);
+    }
+
+    private LauncherInstrumentation.VisibleContainer launchFocusedTask(
+            @Nullable String expectedPackageName) {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to launch focused task: "
+                        + (expectedPackageName == null ? "Overview" : expectedPackageName))) {
+            mLauncher.unpressKeyCode(KeyEvent.KEYCODE_ALT_LEFT, 0);
+            mLauncher.waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
+
+            if (expectedPackageName != null) {
+                mLauncher.assertAppLaunched(expectedPackageName);
+                return mLauncher.getLaunchedAppState();
+            } else {
+                return mLauncher.getOverview();
+            }
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java
new file mode 100644
index 0000000..b7e3d38
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/KeyboardQuickSwitchSource.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import static com.android.launcher3.tapl.LauncherInstrumentation.KEYBOARD_QUICK_SWITCH_RES_ID;
+
+import android.view.KeyEvent;
+
+/**
+ * {@link com.android.launcher3.tapl.LauncherInstrumentation.VisibleContainer} that can be used to
+ * show the keyboard quick switch view.
+ */
+interface KeyboardQuickSwitchSource {
+
+    /**
+     * Shows the Keyboard Quick Switch view.
+     */
+    default KeyboardQuickSwitch showQuickSwitchView() {
+        LauncherInstrumentation launcher = getLauncher();
+
+        try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
+                "want to show keyboard quick switch object")) {
+            launcher.pressAndHoldKeyCode(KeyEvent.KEYCODE_TAB, KeyEvent.META_ALT_LEFT_ON);
+
+            try (LauncherInstrumentation.Closable c2 = launcher.addContextLayer(
+                    "press and held alt+tab")) {
+                launcher.waitForLauncherObject(KEYBOARD_QUICK_SWITCH_RES_ID);
+                launcher.unpressKeyCode(KeyEvent.KEYCODE_TAB, 0);
+
+                return new KeyboardQuickSwitch(
+                        launcher, getStartingContainerType(), isHomeState());
+            }
+        }
+    }
+
+    /** This method requires public access, however should not be called in tests. */
+    LauncherInstrumentation getLauncher();
+
+    /** This method requires public access, however should not be called in tests. */
+    LauncherInstrumentation.ContainerType getStartingContainerType();
+
+    /** This method requires public access, however should not be called in tests. */
+    boolean isHomeState();
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 3450ea7..f6fcfa64 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -20,15 +20,10 @@
 
 import static com.android.launcher3.testing.shared.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
 
-import android.app.UiAutomation;
 import android.graphics.Point;
 import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityEvent;
 
-import androidx.test.uiautomator.By;
-import androidx.test.uiautomator.BySelector;
 import androidx.test.uiautomator.UiObject2;
-import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
@@ -57,7 +52,19 @@
      */
     public LaunchedAppState launch(String expectedPackageName) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
-            return launch(By.pkg(expectedPackageName));
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                    "want to launch an app from " + launchableType())) {
+                LauncherInstrumentation.log("Launchable.launch before click "
+                        + mObject.getVisibleCenter() + " in "
+                        + mLauncher.getVisibleBounds(mObject));
+
+                mLauncher.clickLauncherObject(mObject);
+
+                try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
+                    expectActivityStartEvents();
+                    return mLauncher.assertAppLaunched(expectedPackageName);
+                }
+            }
         }
     }
 
@@ -65,21 +72,6 @@
 
     protected abstract String launchableType();
 
-    private LaunchedAppState launch(BySelector selector) {
-        try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                "want to launch an app from " + launchableType())) {
-            LauncherInstrumentation.log("Launchable.launch before click "
-                    + mObject.getVisibleCenter() + " in " + mLauncher.getVisibleBounds(mObject));
-
-            mLauncher.clickLauncherObject(mObject);
-
-            try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("clicked")) {
-                expectActivityStartEvents();
-                return assertAppLaunched(selector);
-            }
-        }
-    }
-
     /**
      * Clicks a launcher object to initiate splitscreen, where the selected app will be one of two
      * apps running on the screen. Should be called when Launcher is in a "split staging" state
@@ -107,14 +99,6 @@
         }
     }
 
-    protected LaunchedAppState assertAppLaunched(BySelector selector) {
-        mLauncher.assertTrue(
-                "App didn't start: (" + selector + ")",
-                mLauncher.getDevice().wait(Until.hasObject(selector),
-                        LauncherInstrumentation.WAIT_TIME_MS));
-        return new LaunchedAppState(mLauncher);
-    }
-
     Point startDrag(long downTime, Runnable expectLongClickEvents, boolean runToSpringLoadedState) {
         final Point iconCenter = getObject().getVisibleCenter();
         final Point dragStartCenter = new Point(iconCenter.x,
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 30417c0..9f8fb92 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -34,7 +34,6 @@
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 
-import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.Condition;
 import androidx.test.uiautomator.UiDevice;
 
@@ -72,6 +71,11 @@
         return LauncherInstrumentation.ContainerType.LAUNCHED_APP;
     }
 
+    @Override
+    public boolean isHomeState() {
+        return false;
+    }
+
     /**
      * Returns the taskbar.
      *
@@ -200,8 +204,8 @@
 
                     try (LauncherInstrumentation.Closable c4 = launcher.addContextLayer(
                             "dropped item")) {
-                        launchable.assertAppLaunched(By.pkg(expectedNewPackageName));
-                        launchable.assertAppLaunched(By.pkg(expectedExistingPackageName));
+                        launcher.assertAppLaunched(expectedNewPackageName);
+                        launcher.assertAppLaunched(expectedExistingPackageName);
                     }
                 }
             }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index b7d2530..130bd3a 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -20,7 +20,10 @@
 import static android.content.pm.PackageManager.DONT_KILL_APP;
 import static android.content.pm.PackageManager.MATCH_ALL;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
+
 import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID;
 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -49,6 +52,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -170,6 +176,7 @@
     private static final String OPEN_FOLDER_RES_ID = "folder_content";
     static final String TASKBAR_RES_ID = "taskbar_view";
     private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
+    static final String KEYBOARD_QUICK_SWITCH_RES_ID = "keyboard_quick_switch_view";
     public static final int WAIT_TIME_MS = 30000;
     static final long DEFAULT_POLL_INTERVAL = 1000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
@@ -755,18 +762,7 @@
         return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
     }
 
-    /**
-     * Resets the frozen recent tasks list if necessary from a previous quickswitch.
-     */
-    public void resetFreezeRecentTaskList() {
-        try {
-            mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to reset fozen recent tasks list", e);
-        }
-    }
-
-    private UiObject2 verifyContainerType(ContainerType containerType) {
+    UiObject2 verifyContainerType(ContainerType containerType) {
         waitForLauncherInitialized();
 
         if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
@@ -795,6 +791,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -809,6 +806,7 @@
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -824,6 +822,7 @@
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForLauncherObject(APPS_RES_ID);
                 }
@@ -832,6 +831,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -852,6 +852,7 @@
                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     }
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
@@ -866,6 +867,7 @@
                     }
 
                     waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case LAUNCHED_APP: {
@@ -874,6 +876,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (mIgnoreTaskbarVisibility) {
                         return null;
@@ -992,6 +995,25 @@
     }
 
     /**
+     * Goes to home from immersive fullscreen app by first swiping up to bring navbar, and then
+     * performing {@code goHome()} action.
+     * Currently only supports gesture navigation mode.
+     *
+     * @return the Workspace object.
+     */
+    public Workspace goHomeFromImmersiveFullscreenApp() {
+        assertTrue("expected gesture navigation mode",
+                getNavigationModel() == NavigationModel.ZERO_BUTTON);
+        final Point displaySize = getRealDisplaySize();
+        linearGesture(
+                displaySize.x / 2, displaySize.y - 1,
+                displaySize.x / 2, 0,
+                ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
+                false, GestureScope.EXPECT_PILFER);
+        return goHome();
+    }
+
+    /**
      * Goes to home by swiping up in zero-button mode or pressing Home button.
      * Calling it after another TAPL call is safe because all TAPL methods wait for the animations
      * to finish.
@@ -1181,6 +1203,14 @@
         }
     }
 
+    LaunchedAppState assertAppLaunched(@NonNull String expectedPackageName) {
+        BySelector packageSelector = By.pkg(expectedPackageName);
+        assertTrue("App didn't start: (" + packageSelector + ")",
+                mDevice.wait(Until.hasObject(packageSelector),
+                        LauncherInstrumentation.WAIT_TIME_MS));
+        return new LaunchedAppState(this);
+    }
+
     void waitUntilLauncherObjectGone(String resId) {
         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
     }
@@ -1746,6 +1776,12 @@
                 InputDevice.SOURCE_TOUCHSCREEN);
     }
 
+    private void injectEvent(InputEvent event) {
+        assertTrue("injectInputEvent failed: event=" + event,
+                mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
+        event.recycle();
+    }
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope, int source) {
         final boolean hasTIS = hasTIS();
@@ -1783,9 +1819,39 @@
                 || action == MotionEvent.ACTION_BUTTON_RELEASE) {
             event.setActionButton(MotionEvent.BUTTON_PRIMARY);
         }
-        assertTrue("injectInputEvent failed",
-                mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
-        event.recycle();
+        injectEvent(event);
+    }
+
+    private KeyEvent createKeyEvent(int keyCode, int metaState, boolean actionDown) {
+        long eventTime = SystemClock.uptimeMillis();
+        return KeyEvent.obtain(
+                eventTime,
+                eventTime,
+                actionDown ? ACTION_DOWN : ACTION_UP,
+                keyCode,
+                /* repeat= */ 0,
+                metaState,
+                KeyCharacterMap.VIRTUAL_KEYBOARD,
+                /* scancode= */ 0,
+                /* flags= */ 0,
+                InputDevice.SOURCE_KEYBOARD,
+                /* characters =*/ null);
+    }
+
+    /**
+     * Sends a {@link KeyEvent} with {@link ACTION_DOWN} for the given key codes without sending
+     * a {@link KeyEvent} with {@link ACTION_UP}.
+     */
+    public void pressAndHoldKeyCode(int keyCode, int metaState) {
+        injectEvent(createKeyEvent(keyCode, metaState, true));
+    }
+
+
+    /**
+     * Sends a {@link KeyEvent} with {@link ACTION_UP} for the given key codes.
+     */
+    public void unpressKeyCode(int keyCode, int metaState) {
+        injectEvent(createKeyEvent(keyCode, metaState, false));
     }
 
     public long movePointer(long downTime, long startTime, long duration, Point from, Point to,
diff --git a/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
index ce1c3c0..2870877 100644
--- a/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/SplitscreenDragSource.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.tapl;
 
-/** Launchable that can serve as a source for dragging and dropping to splitscreen. */
+/** {@link Launchable} that can serve as a source for dragging and dropping to splitscreen. */
 interface SplitscreenDragSource {
 
     /**
@@ -35,5 +35,6 @@
         }
     }
 
+    /** This method requires public access, however should not be called in tests. */
     Launchable getLaunchable();
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
index c1234fe..3d39041 100644
--- a/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAllApps.java
@@ -73,4 +73,9 @@
     protected void verifyVisibleContainerOnDismiss() {
         mLauncher.getLaunchedAppState().assertTaskbarVisible();
     }
+
+    @Override
+    public boolean isHomeState() {
+        return false;
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 79b54ba..105bc3b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -34,7 +34,8 @@
 /**
  * All widgets container.
  */
-public final class Widgets extends LauncherInstrumentation.VisibleContainer {
+public final class Widgets extends LauncherInstrumentation.VisibleContainer
+        implements KeyboardQuickSwitchSource {
     private static final int FLING_STEPS = 10;
     private static final int SCROLL_ATTEMPTS = 60;
 
@@ -43,6 +44,21 @@
         verifyActiveContainer();
     }
 
+    @Override
+    public LauncherInstrumentation getLauncher() {
+        return mLauncher;
+    }
+
+    @Override
+    public LauncherInstrumentation.ContainerType getStartingContainerType() {
+        return getContainerType();
+    }
+
+    @Override
+    public boolean isHomeState() {
+        return true;
+    }
+
     /**
      * Flings forward (down) and waits the fling's end.
      */
diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
index 141476c..5a4d562 100644
--- a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
@@ -19,7 +19,7 @@
 
 import java.util.function.Supplier;
 
-/** Launchable that can serve as a source for dragging and dropping to the workspace. */
+/** {@link Launchable} that can serve as a source for dragging and dropping to the workspace. */
 interface WorkspaceDragSource {
 
     /**