Merge "Refactor the BorderAnimator for simplicity" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index b4132e4..9221f6b 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -27,3 +27,10 @@
     description: "Enables cursor hover states for certain elements."
     bug: "243191650"
 }
+
+flag {
+    name: "enable_responsive_workspace"
+    namespace: "launcher"
+    description: "Enables new workspace grid calculations method."
+    bug: "241386436"
+}
diff --git a/go/quickstep/res/values-el/strings.xml b/go/quickstep/res/values-el/strings.xml
index 9a67420..7038c11 100644
--- a/go/quickstep/res/values-el/strings.xml
+++ b/go/quickstep/res/values-el/strings.xml
@@ -14,7 +14,7 @@
     <string name="assistant_not_selected_text" msgid="3244613673884359276">"Για να ακούσετε ή να μεταφράσετε κείμενο στην οθόνη σας, επιλέξτε μια εφαρμογή ψηφιακού βοηθού στις Ρυθμίσεις."</string>
     <string name="assistant_not_supported_title" msgid="1675788067597484142">"Αλλάξτε τον βοηθό σας για να χρησιμοποιήσετε αυτήν τη λειτουργία"</string>
     <string name="assistant_not_supported_text" msgid="1708031078549268884">"Για να ακούσετε ή να μεταφράσετε κείμενο στην οθόνη σας, αλλάξτε την εφαρμογή ψηφιακού βοηθού στις Ρυθμίσεις."</string>
-    <string name="tooltip_listen" msgid="7634466447860989102">"Πατήστε εδώ για να ακούσετε το κείμενο σε αυτήν την οθόνη"</string>
-    <string name="tooltip_translate" msgid="4184845868901542567">"Πατήστε εδώ για να μεταφράσετε το κείμενο σε αυτήν την οθόνη"</string>
+    <string name="tooltip_listen" msgid="7634466447860989102">"Πατήστε εδώ για να ακούσετε το κείμενο σε αυτή την οθόνη"</string>
+    <string name="tooltip_translate" msgid="4184845868901542567">"Πατήστε εδώ για να μεταφράσετε το κείμενο σε αυτή την οθόνη"</string>
     <string name="toast_p2p_app_not_shareable" msgid="7229739094132131536">"Δεν είναι δυνατή η κοινή χρήση της εφαρμογής"</string>
 </resources>
diff --git a/quickstep/res/values-my/strings.xml b/quickstep/res/values-my/strings.xml
index 61b1c58..6f1a276 100644
--- a/quickstep/res/values-my/strings.xml
+++ b/quickstep/res/values-my/strings.xml
@@ -24,7 +24,7 @@
     <string name="recents_empty_message" msgid="7040467240571714191">"မကြာမီကဖွင့်ထားသည်များ မရှိပါ"</string>
     <string name="accessibility_app_usage_settings" msgid="6312864233673544149">"အက်ပ်အသုံးပြုမှု ဆက်တင်များ"</string>
     <string name="recents_clear_all" msgid="5328176793634888831">"အားလုံးရှင်းရန်"</string>
-    <string name="accessibility_recent_apps" msgid="4058661986695117371">"လတ်တလောသုံး အက်ပ်များ"</string>
+    <string name="accessibility_recent_apps" msgid="4058661986695117371">"မကြာသေးမီက အက်ပ်များ"</string>
     <string name="task_view_closed" msgid="9170038230110856166">"လုပ်ဆောင်စရာ ပိတ်ထားသည်"</string>
     <string name="task_contents_description_with_remaining_time" msgid="4479688746574672685">"<xliff:g id="TASK_DESCRIPTION">%1$s</xliff:g>၊ <xliff:g id="REMAINING_TIME">%2$s</xliff:g>"</string>
     <string name="shorter_duration_less_than_one_minute" msgid="4722015666335015336">"&lt; ၁ မိနစ်"</string>
diff --git a/quickstep/res/values-sw600dp/config.xml b/quickstep/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..b22cfc5
--- /dev/null
+++ b/quickstep/res/values-sw600dp/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+    <!-- The number of thumbnails and icons to keep in the cache. The thumbnail cache size also
+         determines how many thumbnails will be fetched in the background. -->
+    <integer name="recentsThumbnailCacheSize">8</integer>
+</resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 841fc8f..ce644dc 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1850,7 +1850,7 @@
                     return null;
                 }
 
-                current = (View) view.getParent();
+                current = (View) current.getParent();
             }
 
             return (T) current;
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index f82474a..1440498 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.allapps.FloatingHeaderRow;
 import com.android.launcher3.allapps.FloatingHeaderView;
 import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.keyboard.FocusIndicatorHelper;
 import com.android.launcher3.keyboard.FocusIndicatorHelper.SimpleFocusIndicatorHelper;
 import com.android.launcher3.model.data.ItemInfo;
@@ -126,6 +127,10 @@
         int verticalPadding = getResources().getDimensionPixelSize(
                 R.dimen.all_apps_predicted_icon_vertical_padding);
         int totalHeight = iconHeight + iconPadding + textHeight + verticalPadding * 2;
+        if (FeatureFlags.enableTwolineAllapps()) {
+            // Add extra textHeight to the existing total height.
+            totalHeight += textHeight;
+        }
         return getVisibility() == GONE ? 0 : totalHeight + getPaddingTop() + getPaddingBottom();
     }
 
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
new file mode 100644
index 0000000..6fe007c
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -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.desktop
+
+import android.app.IApplicationThread
+import android.os.IBinder
+import android.os.RemoteException
+import android.util.Log
+import android.view.SurfaceControl
+import android.window.IRemoteTransition
+import android.window.IRemoteTransitionFinishedCallback
+import android.window.RemoteTransition
+import android.window.TransitionInfo
+import com.android.launcher3.statehandlers.DepthController
+import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.quickstep.SystemUiProxy
+import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.views.DesktopTaskView
+import java.util.function.Consumer
+
+/** Manage recents related operations with desktop tasks */
+class DesktopRecentsTransitionController(
+    private val stateManager: StateManager<*>,
+    private val systemUiProxy: SystemUiProxy,
+    private val appThread: IApplicationThread,
+    private val depthController: DepthController?
+) {
+
+    /** Launch desktop tasks from recents view */
+    fun launchDesktopFromRecents(
+        desktopTaskView: DesktopTaskView,
+        callback: Consumer<Boolean>? = null
+    ) {
+        val animRunner =
+            RemoteDesktopLaunchTransitionRunner(
+                desktopTaskView,
+                stateManager,
+                depthController,
+                callback
+            )
+        val transition = RemoteTransition(animRunner, appThread, "RecentsToDesktop")
+        systemUiProxy.showDesktopApps(desktopTaskView.display.displayId, transition)
+    }
+
+    private class RemoteDesktopLaunchTransitionRunner(
+        private val desktopTaskView: DesktopTaskView,
+        private val stateManager: StateManager<*>,
+        private val depthController: DepthController?,
+        private val successCallback: Consumer<Boolean>?
+    ) : IRemoteTransition.Stub() {
+
+        override fun startAnimation(
+            token: IBinder,
+            info: TransitionInfo,
+            t: SurfaceControl.Transaction,
+            finishCallback: IRemoteTransitionFinishedCallback
+        ) {
+            val errorHandlingFinishCallback = Runnable {
+                try {
+                    finishCallback.onTransitionFinished(null /* wct */, null /* sct */)
+                } catch (e: RemoteException) {
+                    Log.e(TAG, "Failed to call finish callback for desktop recents animation", e)
+                }
+            }
+
+            MAIN_EXECUTOR.execute {
+                TaskViewUtils.composeRecentsDesktopLaunchAnimator(
+                    desktopTaskView,
+                    stateManager,
+                    depthController,
+                    info,
+                    t
+                ) {
+                    errorHandlingFinishCallback.run()
+                    successCallback?.accept(true)
+                }
+            }
+        }
+
+        override fun mergeAnimation(
+            transition: IBinder,
+            info: TransitionInfo,
+            t: SurfaceControl.Transaction,
+            mergeTarget: IBinder,
+            finishCallback: IRemoteTransitionFinishedCallback
+        ) {}
+    }
+
+    companion object {
+        const val TAG = "DesktopRecentsTransitionController"
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index fa16b61..df6626a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -88,10 +88,10 @@
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory;
 import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter;
 import com.android.launcher3.util.DimensionUtils;
-import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
@@ -144,6 +144,7 @@
     private int mState;
 
     private final TaskbarActivityContext mContext;
+    private final WindowManagerProxy mWindowManagerProxy;
     private final FrameLayout mNavButtonsView;
     private final LinearLayout mNavButtonContainer;
     // Used for IME+A11Y buttons
@@ -198,10 +199,10 @@
             this::onComputeInsetsForSeparateWindow;
     private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
     private ImageView mRecentsButton;
-    private DisplayController mDisplayController;
 
     public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
         mContext = context;
+        mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
         mNavButtonsView = navButtonsView;
         mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
         mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons);
@@ -232,8 +233,6 @@
                         TaskbarManager.isPhoneMode(deviceProfile));
         mNavButtonsView.getLayoutParams().height = p.y;
 
-        mDisplayController = DisplayController.INSTANCE.get(mContext);
-
         mIsImeRenderingNavButtons =
                 InputMethodService.canImeRenderGesturalNavButtons() && mContext.imeDrawsImeNavBar();
         if (!mIsImeRenderingNavButtons) {
@@ -738,7 +737,8 @@
             NavButtonLayoutter navButtonLayoutter =
                     NavButtonLayoutFactory.Companion.getUiLayoutter(
                             dp, mNavButtonsView, res, isInKidsMode, isInSetup, isThreeButtonNav,
-                            TaskbarManager.isPhoneMode(dp), mDisplayController.getInfo().rotation);
+                            TaskbarManager.isPhoneMode(dp),
+                            mWindowManagerProxy.getRotation(mContext));
             navButtonLayoutter.layoutButtons(dp, isContextualButtonShowing());
             updateNavButtonColor();
             return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index e922c4c..ce901f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -161,6 +161,9 @@
                     if (mActivity != null) {
                         mActivity.removeOnDeviceProfileChangeListener(
                                 mDebugActivityDeviceProfileChanged);
+                        Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                                "unregistering activity lifecycle callbacks from "
+                                        + "onActivityDestroyed.");
                         mActivity.unregisterActivityLifecycleCallbacks(this);
                     }
                     mActivity = null;
@@ -172,6 +175,35 @@
                 }
             };
 
+    UnfoldTransitionProgressProvider.TransitionProgressListener mUnfoldTransitionProgressListener =
+            new UnfoldTransitionProgressProvider.TransitionProgressListener() {
+                @Override
+                public void onTransitionStarted() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition started getting called.");
+                }
+
+                @Override
+                public void onTransitionProgress(float progress) {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition progress : " + progress);
+                }
+
+                @Override
+                public void onTransitionFinishing() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition finishing getting called.");
+
+                }
+
+                @Override
+                public void onTransitionFinished() {
+                    Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                            "fold/unfold transition finished getting called.");
+
+                }
+            };
+
     @SuppressLint("WrongConstant")
     public TaskbarManager(TouchInteractionService service) {
         Display display =
@@ -239,6 +271,7 @@
                 .register(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
         SettingsCache.INSTANCE.get(mContext)
                 .register(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG, "registering component callbacks from constructor.");
         mContext.registerComponentCallbacks(mComponentCallbacks);
         mShutdownReceiver.register(mContext, Intent.ACTION_SHUTDOWN);
         UI_HELPER_EXECUTOR.execute(() -> {
@@ -330,16 +363,18 @@
         if (mActivity == activity) {
             return;
         }
-        if (mActivity != null) {
-            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-            mActivity.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks);
-        }
+        removeActivityCallbacksAndListeners();
         mActivity = activity;
         debugWhyTaskbarNotDestroyed("Set mActivity=" + mActivity);
         mActivity.addOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                "registering activity lifecycle callbacks from setActivity().");
         mActivity.registerActivityLifecycleCallbacks(mLifecycleCallbacks);
         UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
                 getUnfoldTransitionProgressProviderForActivity(activity);
+        if (unfoldTransitionProgressProvider != null) {
+            unfoldTransitionProgressProvider.addCallback(mUnfoldTransitionProgressListener);
+        }
         mUnfoldProgressProvider.setSourceProvider(unfoldTransitionProgressProvider);
 
         if (mTaskbarActivityContext != null) {
@@ -506,15 +541,27 @@
         }
     }
 
+    private void removeActivityCallbacksAndListeners() {
+        if (mActivity != null) {
+            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
+            Log.d(TASKBAR_NOT_DESTROYED_TAG,
+                    "unregistering activity lifecycle callbacks from "
+                            + "removeActivityCallbackAndListeners().");
+            mActivity.unregisterActivityLifecycleCallbacks(mLifecycleCallbacks);
+            UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
+                    getUnfoldTransitionProgressProviderForActivity(mActivity);
+            if (unfoldTransitionProgressProvider != null) {
+                unfoldTransitionProgressProvider.removeCallback(mUnfoldTransitionProgressListener);
+            }
+        }
+    }
+
     /**
      * Called when the manager is no longer needed
      */
     public void destroy() {
         debugWhyTaskbarNotDestroyed("TaskbarManager#destroy()");
-        if (mActivity != null) {
-            mActivity.removeOnDeviceProfileChangeListener(mDebugActivityDeviceProfileChanged);
-        }
-
+        removeActivityCallbacksAndListeners();
         UI_HELPER_EXECUTOR.execute(
                 () -> mTaskbarBroadcastReceiver.unregisterReceiverSafely(mContext));
         destroyExistingTaskbar();
@@ -525,6 +572,7 @@
                 .unregister(USER_SETUP_COMPLETE_URI, mOnSettingsChangeListener);
         SettingsCache.INSTANCE.get(mContext)
                 .unregister(NAV_BAR_KIDS_MODE, mOnSettingsChangeListener);
+        Log.d(TASKBAR_NOT_DESTROYED_TAG, "unregistering component callbacks from destroy().");
         mContext.unregisterComponentCallbacks(mComponentCallbacks);
         mContext.unregisterReceiver(mShutdownReceiver);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 139e8f0..445b312 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -155,7 +155,7 @@
         mControllers.taskbarActivityContext.startTranslationSpring();
     }
 
-    /*
+    /**
      * @param ev MotionEvent in screen coordinates.
      * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
      */
@@ -164,6 +164,14 @@
                 || mControllers.navbarButtonsViewController.isEventOverAnyItem(ev);
     }
 
+    /** Checks if the given {@link MotionEvent} is over the bubble bar stash handle. */
+    public boolean isEventOverBubbleBarStashHandle(MotionEvent ev) {
+        return mControllers.bubbleControllers.map(
+                bubbleControllers ->
+                        bubbleControllers.bubbleStashController.isEventOverStashHandle(ev))
+                .orElse(false);
+    }
+
     /**
      * Returns true if icons should be aligned to hotseat in the current transition.
      */
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/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index c6c4f77..5ce2a7a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -222,7 +222,7 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             mNoIntercept = !mAppsView.shouldContainerScroll(ev)
                     || getTopOpenViewWithType(
-                            mActivityContext, TYPE_ACCESSIBLE & ~TYPE_TASKBAR_ALL_APPS) != null;
+                            mActivityContext, TYPE_ACCESSIBLE & ~TYPE_TASKBAR_OVERLAYS) != null;
         }
         return super.onControllerInterceptTouchEvent(ev);
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index a5ea5a9..09021ed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -22,6 +22,7 @@
 import android.animation.AnimatorSet;
 import android.annotation.Nullable;
 import android.view.InsetsController;
+import android.view.MotionEvent;
 
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.StashedHandleViewController;
@@ -350,4 +351,9 @@
         return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat()
                 : getBubbleBarTranslationYForTaskbar();
     }
+
+    /** Checks whether the motion event is over the stash handle. */
+    public boolean isEventOverStashHandle(MotionEvent ev) {
+        return mHandleViewController.isEventOverHandle(ev);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
index fbab595..c998d97 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java
@@ -24,6 +24,7 @@
 import android.content.res.Resources;
 import android.graphics.Outline;
 import android.graphics.Rect;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
@@ -268,4 +269,22 @@
         });
         return revealAnim;
     }
+
+    /** Checks that the stash handle is visible and that the motion event is within bounds. */
+    public boolean isEventOverHandle(MotionEvent ev) {
+        if (mStashedHandleView.getVisibility() != VISIBLE) {
+            return false;
+        }
+
+        // the bounds of the handle only include the visible part, so we check that the Y coordinate
+        // is anywhere within the stashed taskbar height.
+        int top = mActivity.getDeviceProfile().heightPx - mStashedTaskbarHeight;
+
+        return (int) ev.getRawY() >= top && containsX((int) ev.getRawX());
+    }
+
+    /** Checks if the given x coordinate is within the stashed handle bounds. */
+    public boolean containsX(int x) {
+        return x >= mStashedHandleBounds.left && x <= mStashedHandleBounds.right;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
index 2acd5d4..13ffe6e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneLandscapeNavLayoutter.kt
@@ -57,14 +57,11 @@
             marginStart = 0
         }
 
-        // Swap recents and back button
-        navButtonContainer.addView(recentsButton)
-        navButtonContainer.addView(homeButton)
-        navButtonContainer.addView(backButton)
-
         navButtonContainer.layoutParams = navContainerParams
         navButtonContainer.gravity = Gravity.CENTER
 
+        addThreeButtons()
+
         // Add the spaces in between the nav buttons
         val spaceInBetween: Int =
             resources.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween_phone)
@@ -85,4 +82,11 @@
             }
         }
     }
+
+    open fun addThreeButtons() {
+        // Swap recents and back button
+        navButtonContainer.addView(recentsButton)
+        navButtonContainer.addView(homeButton)
+        navButtonContainer.addView(backButton)
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
index f0fe581..2d62c3f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/navbutton/PhoneSeascapeNavLayoutter.kt
@@ -19,7 +19,6 @@
 import android.content.res.Resources
 import android.view.ViewGroup
 import android.widget.LinearLayout
-import com.android.launcher3.DeviceProfile
 
 class PhoneSeascapeNavLayoutter(
         resources: Resources,
@@ -34,10 +33,7 @@
                 startContextualContainer
         ) {
 
-    override fun layoutButtons(dp: DeviceProfile, isContextualButtonShowing: Boolean) {
-        // TODO(b/230395757): Polish pending, this is just to make it usable
-        super.layoutButtons(dp, isContextualButtonShowing)
-        navButtonContainer.removeAllViews()
+    override fun addThreeButtons() {
         // Flip ordering of back and recents buttons
         navButtonContainer.addView(backButton)
         navButtonContainer.addView(homeButton)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 40909d5..1b22c84 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -106,6 +106,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.appprediction.PredictionRowView;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.hybridhotseat.HotseatPredictionController;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager;
@@ -225,6 +226,9 @@
      */
     private PendingSplitSelectInfo mPendingSplitSelectInfo = null;
 
+    @Nullable
+    private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
+
     private SafeCloseable mViewCapture;
 
     private boolean mEnableWidgetDepth;
@@ -235,12 +239,19 @@
 
         mActionsView = findViewById(R.id.overview_actions_view);
         RecentsView overviewPanel = getOverviewPanel();
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         mSplitSelectStateController =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
                         getDepthController(), getStatsLogManager(),
-                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this),
+                        systemUiProxy, RecentsModel.INSTANCE.get(this),
                         () -> onStateBack());
-        overviewPanel.init(mActionsView, mSplitSelectStateController);
+        if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
+            mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
+                    getStateManager(), systemUiProxy, getIApplicationThread(),
+                    getDepthController());
+        }
+        overviewPanel.init(mActionsView, mSplitSelectStateController,
+                mDesktopRecentsTransitionController);
         mSplitWithKeyboardShortcutController = new SplitWithKeyboardShortcutController(this,
                 mSplitSelectStateController);
         mSplitToWorkspaceController = new SplitToWorkspaceController(this,
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ca4f0ea..e788cc4 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -51,6 +51,7 @@
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.CANCEL_RECENTS_ANIMATION;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED;
+import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.INVALID_VELOCITY_ON_SWIPE_UP;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.LAUNCHER_DESTROYED;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.ON_SETTLED_ON_END_TARGET;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -1214,6 +1215,11 @@
 
     private GestureEndTarget calculateEndTarget(
             PointF velocityPxPerMs, float endVelocityPxPerMs, boolean isFlingY, boolean isCancel) {
+
+        ActiveGestureErrorDetector.GestureEvent gestureEvent =
+                velocityPxPerMs.x == 0 && velocityPxPerMs.y == 0
+                        ? INVALID_VELOCITY_ON_SWIPE_UP
+                        : null;
         ActiveGestureLog.INSTANCE.addLog(
                 new ActiveGestureLog.CompoundString("calculateEndTarget: velocities=(x=")
                         .append(Float.toString(dpiFromPx(velocityPxPerMs.x)))
@@ -1221,7 +1227,7 @@
                         .append(Float.toString(dpiFromPx(velocityPxPerMs.y)))
                         .append("dp/ms), angle=")
                         .append(Double.toString(Math.toDegrees(Math.atan2(
-                                -velocityPxPerMs.y, velocityPxPerMs.x)))));
+                                -velocityPxPerMs.y, velocityPxPerMs.x)))), gestureEvent);
 
         if (mGestureState.isHandlingAtomicEvent()) {
             // Button mode, this is only used to go to recents.
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 3f88139..8925bd6 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -191,8 +191,11 @@
     public abstract boolean allowAllAppsFromOverview();
 
     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        TaskbarUIController controller = getTaskbarController();
+        boolean isEventOverBubbleBarStashHandle =
+                controller != null && controller.isEventOverBubbleBarStashHandle(ev);
         return deviceState.isInDeferredGestureRegion(ev) || deviceState.isImeRenderingNavButtons()
-                || isTrackpadMultiFingerSwipe(ev);
+                || isTrackpadMultiFingerSwipe(ev) || isEventOverBubbleBarStashHandle;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 0a7344a..6ee2cfd 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -353,7 +353,8 @@
         writer.println(prefix + "  ]");
     }
 
-    private static class TaskLoadResult extends ArrayList<GroupTask> {
+    @VisibleForTesting
+    static class TaskLoadResult extends ArrayList<GroupTask> {
 
         final int mRequestId;
 
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index d66421f..3c7fbf0 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -60,6 +60,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
@@ -82,6 +83,7 @@
 import com.android.quickstep.util.RecentsAtomicAnimationFactory;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TISBindHelper;
+import com.android.quickstep.views.DesktopTaskView;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -121,6 +123,8 @@
     private final Handler mHandler = new Handler();
     private final Runnable mAnimationStartTimeoutRunnable = this::onAnimationStartTimeout;
     private SplitSelectStateController mSplitSelectStateController;
+    @Nullable
+    private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
 
     /**
      * Init drag layer and overview panel views.
@@ -133,13 +137,21 @@
         mFallbackRecentsView = findViewById(R.id.overview_panel);
         mActionsView = findViewById(R.id.overview_actions_view);
         getRootView().getSysUiScrim().getSysUIProgress().updateValue(0);
+        SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.get(this);
         mSplitSelectStateController =
                 new SplitSelectStateController(this, mHandler, getStateManager(),
                         null /* depthController */, getStatsLogManager(),
-                        SystemUiProxy.INSTANCE.get(this), RecentsModel.INSTANCE.get(this),
+                        systemUiProxy, RecentsModel.INSTANCE.get(this),
                         null /*activityBackCallback*/);
         mDragLayer.recreateControllers();
-        mFallbackRecentsView.init(mActionsView, mSplitSelectStateController);
+        if (DesktopTaskView.DESKTOP_MODE_SUPPORTED) {
+            mDesktopRecentsTransitionController = new DesktopRecentsTransitionController(
+                    getStateManager(), systemUiProxy, getIApplicationThread(),
+                    null /* depthController */
+            );
+        }
+        mFallbackRecentsView.init(mActionsView, mSplitSelectStateController,
+                mDesktopRecentsTransitionController);
 
         mTISBindHelper = new TISBindHelper(this, this::onTISConnected);
     }
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index d798e62..36a6eb6 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -17,15 +17,18 @@
 
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
+import static com.android.launcher3.config.FeatureFlags.enableGridOnlyOverview;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
+import android.content.ComponentCallbacks;
 import android.content.ComponentCallbacks2;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Process;
 import android.os.UserHandle;
@@ -36,6 +39,7 @@
 import com.android.launcher3.icons.IconProvider.IconChangeListener;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.MainThreadInitializedObject;
+import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -57,7 +61,7 @@
  */
 @TargetApi(Build.VERSION_CODES.O)
 public class RecentsModel implements IconChangeListener, TaskStackChangeListener,
-        TaskVisualsChangeListener {
+        TaskVisualsChangeListener, SafeCloseable {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     public static final MainThreadInitializedObject<RecentsModel> INSTANCE =
@@ -72,17 +76,46 @@
     private final RecentTasksList mTaskList;
     private final TaskIconCache mIconCache;
     private final TaskThumbnailCache mThumbnailCache;
+    private final ComponentCallbacks mCallbacks;
 
     private RecentsModel(Context context) {
-        mContext = context;
-        mTaskList = new RecentTasksList(MAIN_EXECUTOR,
-                context.getSystemService(KeyguardManager.class),
-                SystemUiProxy.INSTANCE.get(context));
+        this(context, new IconProvider(context));
+    }
 
-        IconProvider iconProvider = new IconProvider(context);
-        mIconCache = new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider);
+    private RecentsModel(Context context, IconProvider iconProvider) {
+        this(context,
+                new RecentTasksList(MAIN_EXECUTOR,
+                        context.getSystemService(KeyguardManager.class),
+                        SystemUiProxy.INSTANCE.get(context)),
+                new TaskIconCache(context, RECENTS_MODEL_EXECUTOR, iconProvider),
+                new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR),
+                iconProvider);
+    }
+
+    @VisibleForTesting
+    RecentsModel(Context context, RecentTasksList taskList, TaskIconCache iconCache,
+            TaskThumbnailCache thumbnailCache,
+            IconProvider iconProvider) {
+        mContext = context;
+        mTaskList = taskList;
+        mIconCache = iconCache;
         mIconCache.registerTaskVisualsChangeListener(this);
-        mThumbnailCache = new TaskThumbnailCache(context, RECENTS_MODEL_EXECUTOR);
+        mThumbnailCache = thumbnailCache;
+        if (enableGridOnlyOverview()) {
+            mCallbacks = new ComponentCallbacks() {
+                @Override
+                public void onConfigurationChanged(Configuration configuration) {
+                    updateCacheSizeAndPreloadIfNeeded();
+                }
+
+                @Override
+                public void onLowMemory() {
+                }
+            };
+            context.registerComponentCallbacks(mCallbacks);
+        } else {
+            mCallbacks = null;
+        }
 
         TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
         iconProvider.registerIconChangeListener(this, MAIN_EXECUTOR.getHandler());
@@ -109,7 +142,6 @@
                 RecentsFilterState.DEFAULT_FILTER);
     }
 
-
     /**
      * Fetches the list of recent tasks, based on a filter
      *
@@ -183,8 +215,8 @@
                     // time the user next enters overview
                     continue;
                 }
-                mThumbnailCache.updateThumbnailInCache(group.task1);
-                mThumbnailCache.updateThumbnailInCache(group.task2);
+                mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ true);
+                mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ true);
             }
         });
     }
@@ -281,6 +313,54 @@
     }
 
     /**
+     * Preloads cache if enableGridOnlyOverview is true, preloading is enabled and
+     * highResLoadingState is enabled
+     */
+    public void preloadCacheIfNeeded() {
+        if (!enableGridOnlyOverview()) {
+            return;
+        }
+
+        if (!mThumbnailCache.isPreloadingEnabled()) {
+            // Skip if we aren't preloading.
+            return;
+        }
+
+        if (!mThumbnailCache.getHighResLoadingState().isEnabled()) {
+            // Skip if high-res loading state is disabled.
+            return;
+        }
+
+        mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), taskGroups -> {
+            for (GroupTask group : taskGroups) {
+                mThumbnailCache.updateThumbnailInCache(group.task1, /* lowResolution= */ false);
+                mThumbnailCache.updateThumbnailInCache(group.task2, /* lowResolution= */ false);
+            }
+        });
+    }
+
+    /**
+     * Updates cache size and preloads more tasks if cache size increases
+     */
+    public void updateCacheSizeAndPreloadIfNeeded() {
+        if (!enableGridOnlyOverview()) {
+            return;
+        }
+
+        // If new size is larger than original size, preload more cache to fill the gap
+        if (mThumbnailCache.updateCacheSizeAndRemoveExcess()) {
+            preloadCacheIfNeeded();
+        }
+    }
+
+    @Override
+    public void close() {
+        if (mCallbacks != null) {
+            mContext.unregisterComponentCallbacks(mCallbacks);
+        }
+    }
+
+    /**
      * Listener for receiving running tasks changes
      */
     public interface RunningTasksListener {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 170847d..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;
     }
 
@@ -1268,10 +1270,10 @@
     //
 
     /** Call shell to show all apps active on the desktop */
-    public void showDesktopApps(int displayId) {
+    public void showDesktopApps(int displayId, @Nullable RemoteTransition transition) {
         if (mDesktopMode != null) {
             try {
-                mDesktopMode.showDesktopApps(displayId);
+                mDesktopMode.showDesktopApps(displayId, transition);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call showDesktopApps", e);
             }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
index 3175ba8..2ca9f99 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailCache.java
@@ -15,12 +15,18 @@
  */
 package com.android.quickstep;
 
+import static com.android.launcher3.config.FeatureFlags.enableGridOnlyOverview;
+
 import android.content.Context;
 import android.content.res.Resources;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.R;
 import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.util.CancellableTask;
+import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
+import com.android.quickstep.util.TaskKeyCache;
 import com.android.quickstep.util.TaskKeyLruCache;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
@@ -34,11 +40,10 @@
 public class TaskThumbnailCache {
 
     private final Executor mBgExecutor;
-
-    private final int mCacheSize;
-    private final TaskKeyLruCache<ThumbnailData> mCache;
+    private final TaskKeyCache<ThumbnailData> mCache;
     private final HighResLoadingState mHighResLoadingState;
     private final boolean mEnableTaskSnapshotPreloading;
+    private final Context mContext;
 
     public static class HighResLoadingState {
         private boolean mForceHighResThumbnails;
@@ -91,26 +96,39 @@
     }
 
     public TaskThumbnailCache(Context context, Executor bgExecutor) {
+        this(context, bgExecutor,
+                context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
+    }
+
+    private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
+        this(context, bgExecutor,
+                enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
+                        : new TaskKeyLruCache<>(cacheSize));
+    }
+
+    @VisibleForTesting
+    TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
         mBgExecutor = bgExecutor;
         mHighResLoadingState = new HighResLoadingState(context);
+        mContext = context;
 
         Resources res = context.getResources();
-        mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
         mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
-        mCache = new TaskKeyLruCache<>(mCacheSize);
+        mCache = cache;
     }
 
     /**
-     * Synchronously fetches the thumbnail for the given {@param task} and puts it in the cache.
+     * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
+     * puts it in the cache.
      */
-    public void updateThumbnailInCache(Task task) {
+    public void updateThumbnailInCache(Task task, boolean lowResolution) {
         if (task == null) {
             return;
         }
         Preconditions.assertUIThread();
         // Fetch the thumbnail for this task and put it in the cache
         if (task.thumbnail == null) {
-            updateThumbnailInBackground(task.key, true /* lowResolution */,
+            updateThumbnailInBackground(task.key, lowResolution,
                     t -> task.thumbnail = t);
         }
     }
@@ -148,6 +166,23 @@
         });
     }
 
+    /**
+     * Updates cache size and remove excess entries if current size is more than new cache size.
+     *
+     * @return whether cache size has increased
+     */
+    public boolean updateCacheSizeAndRemoveExcess() {
+        int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
+        int oldSize = mCache.getMaxSize();
+        if (newSize == oldSize) {
+            // Return if no change in size
+            return false;
+        }
+
+        mCache.updateCacheSizeAndRemoveExcess(newSize);
+        return newSize > oldSize;
+    }
+
     private CancellableTask updateThumbnailInBackground(TaskKey key, boolean lowResolution,
             Consumer<ThumbnailData> callback) {
         Preconditions.assertUIThread();
@@ -169,6 +204,16 @@
 
             @Override
             public void handleResult(ThumbnailData result) {
+                // Avoid an async timing issue that a low res entry replaces an existing high res
+                // entry in high res enabled state, so we check before putting it to cache
+                if (enableGridOnlyOverview() && result.reducedResolution
+                        && getHighResLoadingState().isEnabled()) {
+                    ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
+                    if (cachedThumbnail != null && cachedThumbnail.thumbnail != null
+                            && !cachedThumbnail.reducedResolution) {
+                        return;
+                    }
+                }
                 mCache.put(key, result);
                 callback.accept(result);
             }
@@ -195,7 +240,7 @@
      * @return The cache size.
      */
     public int getCacheSize() {
-        return mCacheSize;
+        return mCache.getMaxSize();
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 34965df..819f249 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -613,6 +613,42 @@
         animator.start();
     }
 
+    /**
+     * Start recents to desktop animation
+     */
+    public static void composeRecentsDesktopLaunchAnimator(
+            @NonNull DesktopTaskView launchingTaskView,
+            @NonNull StateManager stateManager, @Nullable DepthController depthController,
+            @NonNull TransitionInfo transitionInfo,
+            SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
+
+        AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                t.apply();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finishCallback.run();
+            }
+        });
+
+        final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps(
+                transitionInfo, t, null /* leashMap */);
+        final RemoteAnimationTarget[] wallpaper = RemoteAnimationTargetCompat.wrapNonApps(
+                transitionInfo, true /* wallpapers */, t, null /* leashMap */);
+        final RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(
+                transitionInfo, false /* wallpapers */, t, null /* leashMap */);
+
+        composeRecentsLaunchAnimator(animatorSet, launchingTaskView, apps, wallpaper, nonApps,
+                true /* launcherClosing */, stateManager, launchingTaskView.getRecentsView(),
+                depthController);
+
+        animatorSet.start();
+    }
+
     public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
             @NonNull RemoteAnimationTarget[] appTargets,
             @NonNull RemoteAnimationTarget[] wallpaperTargets,
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 4c3f738..02d0f39 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -923,8 +923,8 @@
             handleOrientationSetup(base);
         }
         if (mDeviceState.isFullyGesturalNavMode() || newGestureState.isTrackpadGesture()) {
-            String reasonPrefix = "device is in gesture navigation mode or 3-button mode with a"
-                    + "trackpad gesture";
+            String reasonPrefix =
+                    "device is in gesture navigation mode or 3-button mode with a trackpad gesture";
             if (mDeviceState.canTriggerAssistantAction(event)) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
@@ -945,13 +945,18 @@
                     reasonString.append(NEWLINE_PREFIX)
                             .append(reasonPrefix)
                             .append(SUBSTRING_PREFIX)
-                            .append("TaskbarActivityContext != null, "
-                                    + "using TaskbarUnstashInputConsumer");
+                            .append("TaskbarActivityContext != null, ")
+                            .append("using TaskbarUnstashInputConsumer");
                     base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac,
                             mOverviewCommandHelper);
                 }
             } else if (canStartSystemGesture && FeatureFlags.ENABLE_LONG_PRESS_NAV_HANDLE.get()
                     && !previousGestureState.isRecentsAnimationRunning()) {
+                reasonString.append(NEWLINE_PREFIX)
+                        .append(reasonPrefix)
+                        .append(SUBSTRING_PREFIX)
+                        .append("Long press nav handle enabled, ")
+                        .append("using NavHandleLongPressInputConsumer");
                 base = new NavHandleLongPressInputConsumer(this, base, mInputMonitorCompat);
             }
 
@@ -1144,8 +1149,8 @@
         if ((mDeviceState.isFullyGesturalNavMode() || gestureState.isTrackpadGesture())
                 && gestureState.getRunningTask() != null) {
             reasonString.append(SUBSTRING_PREFIX)
-                    .append("device is in gesture nav mode or 3-button mode with a trackpad gesture"
-                            + "and running task != null")
+                    .append("device is in gesture nav mode or 3-button mode with a trackpad")
+                    .append(" gesture and running task != null")
                     .append(", using DeviceLockedInputConsumer");
             return new DeviceLockedInputConsumer(
                     this, mDeviceState, mTaskAnimationManager, gestureState, mInputMonitorCompat);
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 95d88cd..059b0ce 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -73,8 +74,9 @@
     }
 
     @Override
-    public void init(OverviewActionsView actionsView, SplitSelectStateController splitController) {
-        super.init(actionsView, splitController);
+    public void init(OverviewActionsView actionsView, SplitSelectStateController splitController,
+            @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
+        super.init(actionsView, splitController, desktopRecentsTransitionController);
         setOverviewStateEnabled(true);
         setOverlayEnabled(true);
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
index 2abc7ba..ec6efcb 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AccessibilityInputConsumer.java
@@ -152,4 +152,9 @@
             mDelegate.onMotionEvent(ev);
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "AccessibilityInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
index a209b3b..ba012c9 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/AssistantInputConsumer.java
@@ -278,4 +278,9 @@
             return true;
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "AssistantInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
index 858999e..63771f0 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/DelegateInputConsumer.java
@@ -5,6 +5,7 @@
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.quickstep.InputConsumer;
+import com.android.quickstep.util.ActiveGestureLog;
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 public abstract class DelegateInputConsumer implements InputConsumer {
@@ -42,7 +43,15 @@
         mDelegate.onConsumerAboutToBeSwitched();
     }
 
+    /**
+     * Returns the name of this DelegateInputConsumer.
+     */
+    protected abstract String getDelegatorName();
+
     protected void setActive(MotionEvent ev) {
+        ActiveGestureLog.INSTANCE.addLog(new ActiveGestureLog.CompoundString(getDelegatorName())
+                .append(" became active"));
+
         mState = STATE_ACTIVE;
         TestLogging.recordEvent(TestProtocol.SEQUENCE_PILFER, "pilferPointers");
         mInputMonitor.pilferPointers();
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
index 0b11b00..addcfb8 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumer.java
@@ -80,4 +80,9 @@
 
         return distFromMiddle < areaFromMiddle;
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "NavHandleLongPressInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
index 5387c8a..83b556d 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OneHandedModeInputConsumer.java
@@ -175,4 +175,9 @@
         final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
         return angle > ANGLE_MIN && angle < ANGLE_MAX;
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "OneHandedModeInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 200b37d..f6a9440 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -15,6 +15,9 @@
  */
 package com.android.quickstep.inputconsumers;
 
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
@@ -30,6 +33,7 @@
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.InputDevice;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
@@ -68,6 +72,9 @@
     private final boolean mIsTaskbarAllAppsOpen;
     private boolean mHasPassedTaskbarNavThreshold;
     private boolean mIsInBubbleBarArea;
+    private boolean mIsVerticalGestureOverBubbleBar;
+    private boolean mIsPassedBubbleBarSlop;
+    private final int mTouchSlop;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -91,6 +98,7 @@
         mOverviewCommandHelper = overviewCommandHelper;
         // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
         mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
 
         Resources res = context.getResources();
@@ -99,8 +107,7 @@
                 taskbarActivityContext.getDeviceProfile());
         mTaskbarNavThresholdY = taskbarActivityContext.getDeviceProfile().heightPx
                 - mTaskbarNavThreshold;
-        mIsTaskbarAllAppsOpen =
-                mTaskbarActivityContext != null && mTaskbarActivityContext.isTaskbarAllAppsOpen();
+        mIsTaskbarAllAppsOpen = mTaskbarActivityContext.isTaskbarAllAppsOpen();
 
         mIsTransientTaskbar = DisplayController.isTransientTaskbar(context);
 
@@ -132,12 +139,8 @@
         if (mState != STATE_ACTIVE) {
             boolean isStashedTaskbarHovered = isMouseEvent(ev)
                     && isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
-            if (!isStashedTaskbarHovered) {
-                mDelegate.onMotionEvent(ev);
-            }
-
             // Only show the transient task bar if the touch events are on the screen.
-            if (mTaskbarActivityContext != null && !isTrackpadMotionEvent(ev)) {
+            if (!isTrackpadMotionEvent(ev)) {
                 final float x = ev.getRawX();
                 final float y = ev.getRawY();
                 switch (ev.getAction()) {
@@ -193,14 +196,28 @@
                         }
                         mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
 
+                        float dX = mLastPos.x - mDownPos.x;
+                        float dY = mLastPos.y - mDownPos.y;
+
+                        if (!mIsPassedBubbleBarSlop && mIsInBubbleBarArea) {
+                            boolean passedSlop =
+                                    Math.abs(dY) > mTouchSlop || Math.abs(dX) > mTouchSlop;
+                            if (passedSlop) {
+                                mIsPassedBubbleBarSlop = true;
+                                mIsVerticalGestureOverBubbleBar = Math.abs(dY) > Math.abs(dX);
+                                if (mIsVerticalGestureOverBubbleBar) {
+                                    setActive(ev);
+                                }
+                            }
+                        }
+
                         if (mIsTransientTaskbar) {
-                            float dY = mLastPos.y - mDownPos.y;
                             boolean passedTaskbarNavThreshold = dY < 0
                                     && Math.abs(dY) >= mTaskbarNavThreshold;
 
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                if (mIsInBubbleBarArea) {
+                                if (mIsInBubbleBarArea && mIsVerticalGestureOverBubbleBar) {
                                     mTaskbarActivityContext.onSwipeToOpenBubblebar();
                                 } else {
                                     mTaskbarActivityContext.onSwipeToUnstashTaskbar();
@@ -217,17 +234,7 @@
                         break;
                     case MotionEvent.ACTION_UP:
                     case MotionEvent.ACTION_CANCEL:
-                        if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
-                            mTaskbarActivityContext.startTaskbarUnstashHint(
-                                    /* animateForward = */ false);
-                        }
-                        mTaskbarActivityContext.setAutohideSuspendFlag(
-                                FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
-                        if (mTransitionCallback != null) {
-                            mTransitionCallback.onActionEnd();
-                        }
-                        mHasPassedTaskbarNavThreshold = false;
-                        mIsInBubbleBarArea = false;
+                        cleanupAfterMotionEvent();
                         break;
                     case MotionEvent.ACTION_BUTTON_RELEASE:
                         if (isStashedTaskbarHovered) {
@@ -236,9 +243,61 @@
                         break;
                 }
             }
+            boolean isMovingInBubbleBarArea = mIsInBubbleBarArea && ev.getAction() == ACTION_MOVE;
+            if (!isStashedTaskbarHovered) {
+                // if we're moving in the bubble bar area but we haven't passed the slop yet, don't
+                // propagate to the delegate, until we can determine the direction of the gesture.
+                if (!isMovingInBubbleBarArea || mIsPassedBubbleBarSlop) {
+                    mDelegate.onMotionEvent(ev);
+                }
+            }
+        } else if (mIsVerticalGestureOverBubbleBar) {
+            // if we get here then this gesture is a vertical swipe over the bubble bar.
+            // we're also active and there's no need to delegate any additional motion events. the
+            // rest of the gesture will be handled here.
+            switch (ev.getAction()) {
+                case ACTION_MOVE:
+                    int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                    if (pointerIndex == INVALID_POINTER_ID) {
+                        break;
+                    }
+                    mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                    float dY = mLastPos.y - mDownPos.y;
+
+                    // bubble bar swipe gesture uses the same threshold as the taskbar.
+                    boolean passedTaskbarNavThreshold = dY < 0
+                            && Math.abs(dY) >= mTaskbarNavThreshold;
+
+                    if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
+                        mHasPassedTaskbarNavThreshold = true;
+                        mTaskbarActivityContext.onSwipeToOpenBubblebar();
+                    }
+                    break;
+                case ACTION_UP:
+                case ACTION_CANCEL:
+                    cleanupAfterMotionEvent();
+                    break;
+            }
         }
     }
 
+    private void cleanupAfterMotionEvent() {
+        if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
+            mTaskbarActivityContext.startTaskbarUnstashHint(
+                    /* animateForward = */ false);
+        }
+        mTaskbarActivityContext.setAutohideSuspendFlag(
+                FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
+        if (mTransitionCallback != null) {
+            mTransitionCallback.onActionEnd();
+        }
+        mHasPassedTaskbarNavThreshold = false;
+        mIsInBubbleBarArea = false;
+        mIsVerticalGestureOverBubbleBar = false;
+        mIsPassedBubbleBarSlop = false;
+    }
+
     private boolean isInTaskbarArea(float x) {
         float areaFromMiddle = mUnstashArea / 2.0f;
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
@@ -246,13 +305,19 @@
     }
 
     private boolean isInBubbleBarArea(float x) {
-        if (mTaskbarActivityContext != null && mIsTransientTaskbar) {
-            BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
-            if (controllers == null) return false;
+        if (mTaskbarActivityContext == null || !mIsTransientTaskbar) {
+            return false;
+        }
+        BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
+        if (controllers == null) {
+            return false;
+        }
+        if (controllers.bubbleStashController.isStashed()) {
+            return controllers.bubbleStashedHandleViewController.containsX((int) x);
+        } else {
             Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
             return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
         }
-        return false;
     }
 
     private void onLongPressDetected(MotionEvent motionEvent) {
@@ -346,4 +411,9 @@
     private boolean isMouseEvent(MotionEvent event) {
         return event.getSource() == InputDevice.SOURCE_MOUSE;
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "TaskbarUnstashInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
index 7ff9982..f3e21e1 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TrackpadStatusBarInputConsumer.java
@@ -82,4 +82,9 @@
             mSystemUiProxy.onStatusBarTrackpadEvent(ev);
         }
     }
+
+    @Override
+    protected String getDelegatorName() {
+        return "TrackpadStatusBarInputConsumer";
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
index 4d7a5bb..20fa921 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureErrorDetector.java
@@ -39,6 +39,7 @@
         SET_ON_PAGE_TRANSITION_END_CALLBACK, CANCEL_CURRENT_ANIMATION, CLEANUP_SCREENSHOT,
         SCROLLER_ANIMATION_ABORTED, TASK_APPEARED, EXPECTING_TASK_APPEARED,
         FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER, LAUNCHER_DESTROYED, RECENT_TASKS_MISSING,
+        INVALID_VELOCITY_ON_SWIPE_UP,
 
         /**
          * These GestureEvents are specifically associated to state flags that get set in
@@ -66,6 +67,8 @@
 
     private ActiveGestureErrorDetector() {}
 
+    private static final long ON_START_RECENT_ANIMATION_TIME_LIMIT = 500;
+
     protected static void analyseAndDump(
             @NonNull String prefix,
             @NonNull PrintWriter writer,
@@ -76,6 +79,7 @@
         // Use a Set since the order is inherently checked in the loop.
         final Set<GestureEvent> encounteredEvents = new ArraySet<>();
         // Set flags and check order of operations.
+        long lastStartRecentAnimationEventEntryTime = 0;
         for (ActiveGestureLog.EventEntry eventEntry : eventLog.eventEntries) {
             GestureEvent gestureEvent = eventEntry.getGestureEvent();
             if (gestureEvent == null) {
@@ -234,6 +238,16 @@
                             /* errorMessage= */ "ON_START_RECENTS_ANIMATION "
                                     + "onAnimationStart callback ran before startRecentsAnimation",
                             writer);
+                    errorDetected |= printErrorIfTrue(
+                            eventEntry.getTime() - lastStartRecentAnimationEventEntryTime
+                                    > ON_START_RECENT_ANIMATION_TIME_LIMIT,
+                            prefix,
+                            /* errorMessage= */"ON_START_RECENTS_ANIMATION "
+                                    + "startRecentsAnimation was never called or onAnimationStart "
+                                    + "callback was called more than 500 ms after "
+                                    + "startRecentsAnimation.",
+                            writer);
+                    lastStartRecentAnimationEventEntryTime = 0;
                     break;
                 case ON_CANCEL_RECENTS_ANIMATION:
                     errorDetected |= printErrorIfTrue(
@@ -253,12 +267,21 @@
                                     + "callback",
                             writer);
                     break;
+                case INVALID_VELOCITY_ON_SWIPE_UP:
+                    errorDetected |= printErrorIfTrue(
+                            true,
+                            prefix,
+                            /* errorMessage= */ "invalid velocity on swipe up gesture.",
+                            writer);
+                    break;
+                case START_RECENTS_ANIMATION:
+                    lastStartRecentAnimationEventEntryTime = eventEntry.getTime();
+                    break;
                 case MOTION_DOWN:
                 case SET_END_TARGET:
                 case SET_END_TARGET_HOME:
                 case SET_END_TARGET_ALL_APPS:
                 case SET_END_TARGET_NEW_TASK:
-                case START_RECENTS_ANIMATION:
                 case SET_ON_PAGE_TRANSITION_END_CALLBACK:
                 case CANCEL_CURRENT_ANIMATION:
                 case FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER:
diff --git a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
index d6a2e93..7103e63 100644
--- a/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
+++ b/quickstep/src/com/android/quickstep/util/ActiveGestureLog.java
@@ -266,6 +266,10 @@
             time = System.currentTimeMillis();
             duplicateCount = 0;
         }
+
+        public long getTime() {
+            return time;
+        }
     }
 
     /** An entire log of entries associated with a single log ID */
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
index d4923b8..e8c1a78 100644
--- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -36,6 +36,16 @@
         return false;
     }
 
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public boolean isSettingsNavHandleEnabled() {
+        return false;
+    }
+
+    /** Return {@code true} if the Settings toggle is enabled. */
+    public boolean isSettingsHomeButtonEnabled() {
+        return false;
+    }
+
     /** Dump states. */
     public void dump(String prefix, PrintWriter writer) {}
 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
new file mode 100644
index 0000000..79ca076
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCache.java
@@ -0,0 +1,176 @@
+/*
+ * 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.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+
+/**
+ * A class to cache task id and its corresponding object (e.g. thumbnail)
+ *
+ * <p>Maximum size of the cache should be provided when creating this class. When the number of
+ * entries is larger than its max size, it would remove the entry with the smallest last active time
+ * @param <V> Type of object stored in the cache
+ */
+public class TaskKeyByLastActiveTimeCache<V> implements TaskKeyCache<V> {
+    private static final String TAG = TaskKeyByLastActiveTimeCache.class.getSimpleName();
+    private final AtomicInteger mMaxSize;
+    private final Map<Integer, Entry<V>> mMap;
+    // To sort task id by last active time
+    private final PriorityQueue<Task.TaskKey> mQueue;
+
+    public TaskKeyByLastActiveTimeCache(int maxSize) {
+        mMap = new HashMap(maxSize);
+        mQueue = new PriorityQueue<>(Comparator.comparingLong(t -> t.lastActiveTime));
+        mMaxSize = new AtomicInteger(maxSize);
+    }
+
+    /**
+     * Removes all entries from the cache
+     */
+    @Override
+    public synchronized void evictAll() {
+        mMap.clear();
+        mQueue.clear();
+    }
+
+
+    /**
+     * Removes a particular entry from the cache
+     */
+    @Override
+    public synchronized void remove(Task.TaskKey key) {
+        if (key == null) {
+            return;
+        }
+
+        Entry<V> entry = mMap.remove(key.id);
+        if (entry != null) {
+            // Use real key in map entry to handle use case of using stub key for removal
+            mQueue.remove(entry.mKey);
+        }
+    }
+
+    /**
+     * Removes all entries matching keyCheck
+     */
+    @Override
+    public synchronized void removeAll(Predicate<Task.TaskKey> keyCheck) {
+        Iterator<Task.TaskKey> iterator = mQueue.iterator();
+        while (iterator.hasNext()) {
+            Task.TaskKey key = iterator.next();
+            if (keyCheck.test(key)) {
+                mMap.remove(key.id);
+                iterator.remove();
+            }
+        }
+    }
+
+    /**
+     * Gets the entry if it is still valid
+     */
+    @Override
+    public synchronized V getAndInvalidateIfModified(Task.TaskKey key) {
+        Entry<V> entry = mMap.get(key.id);
+        if (entry != null && entry.mKey.windowingMode == key.windowingMode
+                && entry.mKey.lastActiveTime == key.lastActiveTime) {
+            return entry.mValue;
+        } else {
+            remove(key);
+            return null;
+        }
+    }
+
+    /**
+     * Adds an entry to the cache, optionally evicting the last accessed entry
+     */
+    @Override
+    public final synchronized void put(Task.TaskKey key, V value) {
+        if (key != null && value != null) {
+            Entry<V> entry = mMap.get(key.id);
+            // If the same key already exist, remove item for existing key
+            if (entry != null) {
+                mQueue.remove(entry.mKey);
+            }
+
+            mMap.put(key.id, new Entry<>(key, value));
+            mQueue.add(key);
+            removeExcessIfNeeded();
+        } else {
+            Log.e(TAG, "Unexpected null key or value: " + key + ", " + value);
+        }
+    }
+
+    /**
+     * Updates the cache entry if it is already present in the cache
+     */
+    @Override
+    public synchronized void updateIfAlreadyInCache(int taskId, V data) {
+        Entry<V> entry = mMap.get(taskId);
+        if (entry != null) {
+            entry.mValue = data;
+        }
+    }
+
+    /**
+     * Updates cache size and remove excess if the number of existing entries is larger than new
+     * cache size
+     */
+    @Override
+    public synchronized void updateCacheSizeAndRemoveExcess(int cacheSize) {
+        mMaxSize.compareAndSet(mMaxSize.get(), cacheSize);
+        removeExcessIfNeeded();
+    }
+
+    private synchronized void removeExcessIfNeeded() {
+        while (mQueue.size() > mMaxSize.get() && !mQueue.isEmpty()) {
+            Task.TaskKey key = mQueue.poll();
+            mMap.remove(key.id);
+        }
+    }
+
+    /**
+     * Get maximum size of the cache
+     */
+    @Override
+    public int getMaxSize() {
+        return mMaxSize.get();
+    }
+
+    /**
+     * Get current size of the cache
+     */
+    @Override
+    public int getSize() {
+        return mMap.size();
+    }
+
+    @VisibleForTesting
+    PriorityQueue<Task.TaskKey> getQueue() {
+        return mQueue;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
new file mode 100644
index 0000000..8ee78ab
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyCache.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.android.systemui.shared.recents.model.Task;
+
+import java.util.function.Predicate;
+
+/**
+ * An interface for caching task id and its corresponding object (e.g. thumbnail, task icon)
+ *
+ * @param <V> Type of object stored in the cache
+ */
+public interface TaskKeyCache<V> {
+
+    /**
+     * Removes all entries from the cache.
+     */
+    void evictAll();
+
+    /**
+     * Removes a particular entry from the cache.
+     */
+    void remove(Task.TaskKey key);
+
+    /**
+     * Removes all entries matching keyCheck.
+     */
+    void removeAll(Predicate<Task.TaskKey> keyCheck);
+
+    /**
+     * Gets the entry if it is still valid.
+     */
+    V getAndInvalidateIfModified(Task.TaskKey key);
+
+    /**
+     * Adds an entry to the cache, optionally evicting the last accessed entry.
+     */
+    void put(Task.TaskKey key, V value);
+
+    /**
+     * Updates the cache entry if it is already present in the cache.
+     */
+    void updateIfAlreadyInCache(int taskId, V data);
+
+    /**
+     * Updates cache size and remove excess if the number of existing entries is larger than new
+     * cache size.
+     */
+    default void updateCacheSizeAndRemoveExcess(int cacheSize) { }
+
+    /**
+     * Gets maximum size of the cache.
+     */
+    int getMaxSize();
+
+    /**
+     * Gets current size of the cache.
+     */
+    int getSize();
+
+    class Entry<V> {
+
+        final Task.TaskKey mKey;
+        V mValue;
+
+        Entry(Task.TaskKey key, V value) {
+            mKey = key;
+            mValue = value;
+        }
+
+        @Override
+        public int hashCode() {
+            return mKey.id;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
index 08a65fa..89f5d41 100644
--- a/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
+++ b/quickstep/src/com/android/quickstep/util/TaskKeyLruCache.java
@@ -27,7 +27,7 @@
  * A simple LRU cache for task key entries
  * @param <V> The type of the value
  */
-public class TaskKeyLruCache<V> {
+public class TaskKeyLruCache<V> implements TaskKeyCache<V> {
 
     private final MyLinkedHashMap<V> mMap;
 
@@ -92,20 +92,14 @@
         }
     }
 
-    private static class Entry<V> {
+    @Override
+    public int getMaxSize() {
+        return mMap.mMaxSize;
+    }
 
-        final TaskKey mKey;
-        V mValue;
-
-        Entry(TaskKey key, V value) {
-            mKey = key;
-            mValue = value;
-        }
-
-        @Override
-        public int hashCode() {
-            return mKey.id;
-        }
+    @Override
+    public int getSize() {
+        return mMap.size();
     }
 
     private static class MyLinkedHashMap<V> extends LinkedHashMap<Integer, Entry<V>> {
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 0b223a7..8a6c197 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -44,10 +44,10 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.RunnableList;
 import com.android.quickstep.RecentsModel;
-import com.android.quickstep.SystemUiProxy;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
@@ -334,17 +334,21 @@
         return false;
     }
 
-    @Override
-    public RunnableList launchTasks() {
-        SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps(mActivity.getDisplayId());
-        Launcher.getLauncher(mActivity).getStateManager().goToState(NORMAL, false /* animated */);
-        return null;
-    }
-
     @Nullable
     @Override
     public RunnableList launchTaskAnimated() {
-        return launchTasks();
+        RunnableList endCallback = new RunnableList();
+        endCallback.add(() -> Launcher.getLauncher(mActivity).getStateManager().goToState(NORMAL));
+
+        DesktopRecentsTransitionController recentsController =
+                getRecentsView().getDesktopRecentsController();
+        if (recentsController != null) {
+            recentsController.launchDesktopFromRecents(this, success -> {
+                endCallback.executeAllAndDestroy();
+            });
+        }
+
+        return endCallback;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 892ebf9..1867fe9 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
@@ -75,8 +76,9 @@
 
     @Override
     public void init(OverviewActionsView actionsView,
-            SplitSelectStateController splitPlaceholderView) {
-        super.init(actionsView, splitPlaceholderView);
+            SplitSelectStateController splitPlaceholderView,
+            @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
+        super.init(actionsView, splitPlaceholderView, desktopRecentsTransitionController);
         setContentAlpha(0);
     }
 
@@ -276,7 +278,8 @@
             desktopVisibilityController.setRecentsGestureEnd(endTarget);
         }
         if (showDesktopApps) {
-            SystemUiProxy.INSTANCE.get(mActivity).showDesktopApps(mActivity.getDisplayId());
+            SystemUiProxy.INSTANCE.get(mActivity).showDesktopApps(mActivity.getDisplayId(),
+                    null /* transition */);
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9b41ccd..825c0ae 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -143,6 +143,7 @@
 import com.android.launcher3.anim.SpringProperty;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.desktop.DesktopRecentsTransitionController;
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DepthController;
@@ -722,6 +723,9 @@
     private ObjectAnimator mActionsViewAlphaAnimator;
     private float mActionsViewAlphaAnimatorFinalValue;
 
+    @Nullable
+    private DesktopRecentsTransitionController mDesktopRecentsTransitionController;
+
     /**
      * Keeps track of the desktop task. Optional and only present when the feature flag is enabled.
      */
@@ -1042,10 +1046,12 @@
         updateTaskStackListenerState();
     }
 
-    public void init(OverviewActionsView actionsView, SplitSelectStateController splitController) {
+    public void init(OverviewActionsView actionsView, SplitSelectStateController splitController,
+            @Nullable DesktopRecentsTransitionController desktopRecentsTransitionController) {
         mActionsView = actionsView;
         mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
         mSplitSelectStateController = splitController;
+        mDesktopRecentsTransitionController = desktopRecentsTransitionController;
     }
 
     public SplitSelectStateController getSplitSelectController() {
@@ -2271,10 +2277,12 @@
         if (showAsGrid()) {
             int screenStart = mOrientationHandler.getPrimaryScroll(this);
             int pageOrientedSize = mOrientationHandler.getMeasuredSize(this);
-            int halfScreenSize = pageOrientedSize / 2;
-            // Use +/- 50% screen width as visible area.
-            visibleStart = screenStart - halfScreenSize;
-            visibleEnd = screenStart + pageOrientedSize + halfScreenSize;
+            // For GRID_ONLY_OVERVIEW, use +/- 1 task column as visible area for preloading
+            // adjacent thumbnails, otherwise use +/-50% screen width
+            int extraWidth = enableGridOnlyOverview() ? getLastComputedTaskSize().width()
+                    + getPageSpacing() : pageOrientedSize / 2;
+            visibleStart = screenStart - extraWidth;
+            visibleEnd = screenStart + pageOrientedSize + extraWidth;
         } else {
             int centerPageIndex = getPageNearestToCenterOfScreen();
             int numChildren = getChildCount();
@@ -2355,6 +2363,12 @@
 
     @Override
     public void onHighResLoadingStateChanged(boolean enabled) {
+        // Preload cache when no overview task is visible (e.g. not in overview page), so when
+        // user goes to overview next time, the task thumbnails would show up without delay
+        if (mHasVisibleTaskData.size() == 0) {
+            mModel.preloadCacheIfNeeded();
+        }
+
         // Whenever the high res loading state changes, poke each of the visible tasks to see if
         // they want to updated their thumbnail state
         for (int i = 0; i < mHasVisibleTaskData.size(); i++) {
@@ -5787,6 +5801,11 @@
         return null;
     }
 
+    @Nullable
+    protected DesktopRecentsTransitionController getDesktopRecentsController() {
+        return mDesktopRecentsTransitionController;
+    }
+
     /** Enables or disables modal state for RecentsView */
     public abstract void setModalStateEnabled(int taskId, boolean animate);
 
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 1ffef9b..50b38ea 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -107,7 +107,6 @@
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskCornerRadius;
 import com.android.quickstep.util.TaskRemovedDuringLaunchListener;
-import com.android.quickstep.util.TransformParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -975,19 +974,17 @@
             if (remoteTargetHandles.length == 1) {
                 targets = remoteTargetHandles[0].getTransformParams().getTargetSet();
             } else {
-                TransformParams topLeftParams = remoteTargetHandles[0].getTransformParams();
-                TransformParams rightBottomParams = remoteTargetHandles[1].getTransformParams();
-                RemoteAnimationTarget[] apps = Stream.concat(
-                        Arrays.stream(topLeftParams.getTargetSet().apps),
-                        Arrays.stream(rightBottomParams.getTargetSet().apps))
+                RemoteAnimationTarget[] apps = Arrays.stream(remoteTargetHandles)
+                        .flatMap(handle -> Stream.of(
+                                handle.getTransformParams().getTargetSet().apps))
                         .toArray(RemoteAnimationTarget[]::new);
-                RemoteAnimationTarget[] wallpapers = Stream.concat(
-                        Arrays.stream(topLeftParams.getTargetSet().wallpapers),
-                        Arrays.stream(rightBottomParams.getTargetSet().wallpapers))
+                RemoteAnimationTarget[] wallpapers = Arrays.stream(remoteTargetHandles)
+                        .flatMap(handle -> Stream.of(
+                                handle.getTransformParams().getTargetSet().wallpapers))
                         .toArray(RemoteAnimationTarget[]::new);
                 targets = new RemoteAnimationTargets(apps, wallpapers,
-                        topLeftParams.getTargetSet().nonApps,
-                        topLeftParams.getTargetSet().targetMode);
+                        remoteTargetHandles[0].getTransformParams().getTargetSet().nonApps,
+                        remoteTargetHandles[0].getTransformParams().getTargetSet().targetMode);
             }
             if (targets == null) {
                 // If the recents animation is cancelled somehow between the parent if block and
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index a10b24d..7d82944 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -61,7 +61,6 @@
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
-import com.android.launcher3.util.rule.TestIsolationRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 import com.android.quickstep.views.RecentsView;
@@ -95,6 +94,9 @@
     public final TestRule mDisableHeadsUpNotification = disableHeadsUpNotification();
 
     @Rule
+    public final TestRule mSetLauncherCommand;
+
+    @Rule
     public final TestRule mOrderSensitiveRules;
 
     @Rule
@@ -114,7 +116,19 @@
             Utilities.enableRunningInTestHarnessForTests();
         }
 
-        final TestRule setLauncherCommand = (base, desc) -> new Statement() {
+        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
+                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
+        mOrderSensitiveRules = RuleChain
+                .outerRule(new SamplerRule())
+                .around(new NavigationModeSwitchRule(mLauncher))
+                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
+                .around(viewCaptureRule);
+
+        mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
+                getHomeIntentInPackage(context),
+                MATCH_DISABLED_COMPONENTS).get(0).activityInfo;
+
+        mSetLauncherCommand = (base, desc) -> new Statement() {
             @Override
             public void evaluate() throws Throwable {
                 TestCommandReceiver.callCommand(TestCommandReceiver.ENABLE_TEST_LAUNCHER);
@@ -138,21 +152,6 @@
             }
         };
 
-        final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(
-                RecentsActivity.ACTIVITY_TRACKER::getCreatedActivity);
-        mOrderSensitiveRules = RuleChain
-                .outerRule(new SamplerRule())
-                .around(new TestStabilityRule())
-                .around(new NavigationModeSwitchRule(mLauncher))
-                .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule)
-                .around(new TestIsolationRule(mLauncher))
-                .around(setLauncherCommand);
-
-        mOtherLauncherActivity = context.getPackageManager().queryIntentActivities(
-                getHomeIntentInPackage(context),
-                MATCH_DISABLED_COMPONENTS).get(0).activityInfo;
-
         if (TestHelpers.isInLauncherProcess()) {
             mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
                     TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
new file mode 100644
index 0000000..08e0898
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/RecentsModelTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.Flags;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconProvider;
+import com.android.quickstep.util.GroupTask;
+import com.android.systemui.shared.recents.model.Task;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+@SmallTest
+public class RecentsModelTest {
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private TaskThumbnailCache mThumbnailCache;
+
+    @Mock
+    private RecentTasksList mTasksList;
+
+    @Mock
+    private TaskThumbnailCache.HighResLoadingState mHighResLoadingState;
+
+    private RecentsModel mRecentsModel;
+
+    private RecentTasksList.TaskLoadResult mTaskResult;
+
+    private Resources mResource;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setup() throws NoSuchFieldException {
+        MockitoAnnotations.initMocks(this);
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW);
+        mTaskResult = getTaskResult();
+        doAnswer(invocation-> {
+            Consumer<ArrayList<GroupTask>> callback = invocation.getArgument(1);
+            callback.accept(mTaskResult);
+            return null;
+        }).when(mTasksList).getTaskKeys(anyInt(), any());
+
+        when(mHighResLoadingState.isEnabled()).thenReturn(true);
+        when(mThumbnailCache.getHighResLoadingState()).thenReturn(mHighResLoadingState);
+        when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
+
+        mRecentsModel = new RecentsModel(mContext, mTasksList, mock(TaskIconCache.class),
+                mThumbnailCache, mock(IconProvider.class));
+
+        mResource = mock(Resources.class);
+        when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+        when(mContext.getResources()).thenReturn(mResource);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW);
+    }
+
+    @Test
+    @UiThreadTest
+    public void preloadOnHighResolutionEnabled() {
+        mRecentsModel.preloadCacheIfNeeded();
+
+        ArgumentCaptor<Task> taskArgs = ArgumentCaptor.forClass(Task.class);
+        verify(mRecentsModel.getThumbnailCache(), times(2))
+                .updateThumbnailInCache(taskArgs.capture(), /* lowResolution= */ eq(false));
+
+        GroupTask expectedGroupTask = mTaskResult.get(0);
+        assertThat(taskArgs.getAllValues().get(0)).isEqualTo(
+                expectedGroupTask.task1);
+        assertThat(taskArgs.getAllValues().get(1)).isEqualTo(
+                expectedGroupTask.task2);
+    }
+
+    @Test
+    public void notPreloadOnHighResolutionDisabled() {
+        when(mHighResLoadingState.isEnabled()).thenReturn(false);
+        when(mThumbnailCache.isPreloadingEnabled()).thenReturn(true);
+        mRecentsModel.preloadCacheIfNeeded();
+        verify(mRecentsModel.getThumbnailCache(), never())
+                .updateThumbnailInCache(any(), anyBoolean());
+    }
+
+    @Test
+    public void notPreloadOnPreloadDisabled() {
+        when(mThumbnailCache.isPreloadingEnabled()).thenReturn(false);
+        mRecentsModel.preloadCacheIfNeeded();
+        verify(mRecentsModel.getThumbnailCache(), never())
+                .updateThumbnailInCache(any(), anyBoolean());
+
+    }
+
+    @Test
+    public void increaseCacheSizeAndPreload() {
+        // Mock to return preload is needed
+        when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(true);
+        // Update cache size
+        mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
+        // Assert update cache is called
+        verify(mRecentsModel.getThumbnailCache(), times(2))
+                .updateThumbnailInCache(any(), /* lowResolution= */ eq(false));
+    }
+
+    @Test
+    public void decreaseCacheSizeAndNotPreload() {
+        // Mock to return preload is not needed
+        when(mThumbnailCache.updateCacheSizeAndRemoveExcess()).thenReturn(false);
+        // Update cache size
+        mRecentsModel.updateCacheSizeAndPreloadIfNeeded();
+        // Assert update cache is never called
+        verify(mRecentsModel.getThumbnailCache(), never())
+                .updateThumbnailInCache(any(), anyBoolean());
+    }
+
+    private RecentTasksList.TaskLoadResult getTaskResult() {
+        RecentTasksList.TaskLoadResult allTasks = new RecentTasksList.TaskLoadResult(0, false, 1);
+        ActivityManager.RecentTaskInfo taskInfo1 = new ActivityManager.RecentTaskInfo();
+        Task.TaskKey taskKey1 = new Task.TaskKey(taskInfo1);
+        Task task1 = Task.from(taskKey1, taskInfo1, false);
+
+        ActivityManager.RecentTaskInfo taskInfo2 = new ActivityManager.RecentTaskInfo();
+        Task.TaskKey taskKey2 = new Task.TaskKey(taskInfo2);
+        Task task2 = Task.from(taskKey2, taskInfo2, false);
+
+        allTasks.add(new GroupTask(task1, task2, null));
+        return allTasks;
+    }
+}
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/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java b/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java
new file mode 100644
index 0000000..4e04261
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/TaskThumbnailCacheTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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 static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.R;
+import com.android.quickstep.util.TaskKeyCache;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.Executor;
+
+@SmallTest
+public class TaskThumbnailCacheTest {
+    @Mock
+    private Context mContext;
+
+    @Mock
+    private Resources mResource;
+
+    @Mock
+    private TaskKeyCache mTaskKeyCache;
+
+    @Before
+    public void setup() throws NoSuchFieldException {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getResources()).thenReturn(mResource);
+    }
+
+    @Test
+    public void increaseCacheSize() {
+        // Mock a cache size increase from 3 to 8
+        when(mTaskKeyCache.getMaxSize()).thenReturn(3);
+        when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(8);
+        TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+                mTaskKeyCache);
+
+        // Preload is needed when increasing size
+        assertTrue(thumbnailCache.updateCacheSizeAndRemoveExcess());
+        verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(8);
+    }
+
+    @Test
+    public void decreaseCacheSize() {
+        // Mock a cache size decrease from 8 to 3
+        when(mTaskKeyCache.getMaxSize()).thenReturn(8);
+        when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+        TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+                mTaskKeyCache);
+        // Preload is not needed when decreasing size
+        assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
+        verify(mTaskKeyCache, times(1)).updateCacheSizeAndRemoveExcess(3);
+    }
+
+    @Test
+    public void keepSameCacheSize() {
+        when(mTaskKeyCache.getMaxSize()).thenReturn(3);
+        when(mResource.getInteger((R.integer.recentsThumbnailCacheSize))).thenReturn(3);
+        TaskThumbnailCache thumbnailCache = new TaskThumbnailCache(mContext, mock(Executor.class),
+                mTaskKeyCache);
+        // Preload is not needed when it has the same cache size
+        assertFalse(thumbnailCache.updateCacheSizeAndRemoveExcess());
+        verify(mTaskKeyCache, never()).updateCacheSizeAndRemoveExcess(anyInt());
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java
new file mode 100644
index 0000000..ea2688a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/TaskKeyByLastActiveTimeCacheTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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 static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+import org.junit.Test;
+
+@SmallTest
+public class TaskKeyByLastActiveTimeCacheTest {
+    @Test
+    public void add() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 1);
+        ThumbnailData data1 = new ThumbnailData();
+        cache.put(key1, data1);
+
+        Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+                new ComponentName("", ""), 0, 2);
+        ThumbnailData data2 = new ThumbnailData();
+        cache.put(key2, data2);
+
+        assertEquals(2, cache.getSize());
+        assertEquals(data1, cache.getAndInvalidateIfModified(key1));
+        assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+        assertEquals(2, cache.getQueue().size());
+        assertEquals(key1, cache.getQueue().poll());
+        assertEquals(key2, cache.getQueue().poll());
+    }
+
+    @Test
+    public void addSameTasksWithSameLastActiveTimeTwice() {
+        // Add 2 tasks with same id and last active time, it should only have 1 entry in cache
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 1000);
+        ThumbnailData data1 = new ThumbnailData();
+        cache.put(key1, data1);
+
+        Task.TaskKey key2 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 1000);
+        ThumbnailData data2 = new ThumbnailData();
+        cache.put(key2, data2);
+
+        assertEquals(1, cache.getSize());
+        assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+        assertEquals(1, cache.getQueue().size());
+        assertEquals(key2, cache.getQueue().poll());
+    }
+
+    @Test
+    public void addSameTasksWithDifferentLastActiveTime() {
+        // Add 2 tasks with same id and different last active time, it should only have the
+        // higher last active time entry
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 1000);
+        ThumbnailData data1 = new ThumbnailData();
+        cache.put(key1, data1);
+
+        Task.TaskKey key2 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 2000);
+        ThumbnailData data2 = new ThumbnailData();
+        cache.put(key2, data2);
+
+        assertEquals(1, cache.getSize());
+        assertEquals(data2, cache.getAndInvalidateIfModified(key2));
+
+        assertEquals(1, cache.getQueue().size());
+        Task.TaskKey queueKey = cache.getQueue().poll();
+        assertEquals(key2, queueKey);
+        // TaskKey's equal method does not check last active time, so we check here
+        assertEquals(2000, queueKey.lastActiveTime);
+    }
+
+    @Test
+    public void remove() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 0);
+        cache.put(key1, new ThumbnailData());
+
+        cache.remove(key1);
+
+        assertEquals(0, cache.getSize());
+        assertEquals(0, cache.getQueue().size());
+    }
+
+    @Test
+    public void removeByStubKey() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 1, new Intent(),
+                new ComponentName("", ""), 1, 100);
+        cache.put(key1, new ThumbnailData());
+
+        Task.TaskKey stubKey = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 0);
+        cache.remove(stubKey);
+
+        assertEquals(0, cache.getSize());
+        assertEquals(0, cache.getQueue().size());
+    }
+
+    @Test
+    public void evictAll() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 0);
+        cache.put(key1, new ThumbnailData());
+        Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+                new ComponentName("", ""), 0, 0);
+        cache.put(key2, new ThumbnailData());
+
+        cache.evictAll();
+
+        assertEquals(0, cache.getSize());
+        assertEquals(0, cache.getQueue().size());
+    }
+
+    @Test
+    public void removeAllByPredicate() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        // Add user 1's tasks
+        Task.TaskKey user1Key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 1, 0);
+        cache.put(user1Key1, new ThumbnailData());
+        Task.TaskKey user1Key2 = new Task.TaskKey(2, 0, new Intent(),
+                new ComponentName("", ""), 1, 0);
+        cache.put(user1Key2, new ThumbnailData());
+        // Add user 2's task
+        Task.TaskKey user2Key = new Task.TaskKey(3, 0, new Intent(),
+                new ComponentName("", ""), 2, 0);
+        ThumbnailData user2Data = new ThumbnailData();
+        cache.put(user2Key, user2Data);
+
+        cache.removeAll(key -> key.userId == 1);
+
+        // Only user 2's task remains
+        assertEquals(1, cache.getSize());
+        assertEquals(user2Data, cache.getAndInvalidateIfModified(user2Key));
+
+        assertEquals(1, cache.getQueue().size());
+        assertEquals(user2Key, cache.getQueue().poll());
+    }
+
+    @Test
+    public void getAndInvalidateIfModified() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(3);
+        // Add user 1's tasks
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 1, 0);
+        ThumbnailData data1 = new ThumbnailData();
+        cache.put(key1, data1);
+
+        // Get result with task key of same last active time
+        Task.TaskKey keyWithSameActiveTime = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 1, 0);
+        ThumbnailData result1 = cache.getAndInvalidateIfModified(keyWithSameActiveTime);
+        assertEquals(data1, result1);
+        assertEquals(1, cache.getQueue().size());
+
+        // Invalidate result with task key of new last active time
+        Task.TaskKey keyWithNewActiveTime = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 1, 1);
+        ThumbnailData result2 = cache.getAndInvalidateIfModified(keyWithNewActiveTime);
+        // No entry is retrieved because the key has higher last active time
+        assertNull(result2);
+        assertEquals(0, cache.getSize());
+        assertEquals(0, cache.getQueue().size());
+    }
+
+    @Test
+    public void removeByLastActiveTimeWhenOverMaxSize() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(2);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 200);
+        ThumbnailData task1 = new ThumbnailData();
+        cache.put(key1, task1);
+        Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+                new ComponentName("", ""), 0, 100);
+        ThumbnailData task2 = new ThumbnailData();
+        cache.put(key2, task2);
+
+        // Add the 3rd entry which will exceed the max cache size
+        Task.TaskKey key3 = new Task.TaskKey(3, 0, new Intent(),
+                new ComponentName("", ""), 0, 300);
+        ThumbnailData task3 = new ThumbnailData();
+        cache.put(key3, task3);
+
+        // Assert map size and check the remaining entries have higher active time
+        assertEquals(2, cache.getSize());
+        assertEquals(task1, cache.getAndInvalidateIfModified(key1));
+        assertEquals(task3, cache.getAndInvalidateIfModified(key3));
+        assertNull(cache.getAndInvalidateIfModified(key2));
+
+        // Assert queue size and check the remaining entries have higher active time
+        assertEquals(2, cache.getQueue().size());
+        Task.TaskKey queueKey1 = cache.getQueue().poll();
+        assertEquals(key1, queueKey1);
+        assertEquals(200, queueKey1.lastActiveTime);
+        Task.TaskKey queueKey2 = cache.getQueue().poll();
+        assertEquals(key3, queueKey2);
+        assertEquals(300, queueKey2.lastActiveTime);
+    }
+
+    @Test
+    public void updateIfAlreadyInCache() {
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(2);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 200);
+        cache.put(key1, new ThumbnailData());
+
+        // Update original data to new data
+        ThumbnailData newData = new ThumbnailData();
+        cache.updateIfAlreadyInCache(key1.id, newData);
+
+        // Data is updated to newData successfully
+        ThumbnailData result = cache.getAndInvalidateIfModified(key1);
+        assertEquals(newData, result);
+    }
+
+    @Test
+    public void updateCacheSizeAndInvalidateExcess() {
+        // Last active time are not in-sync with insertion order to simulate the real async case
+        TaskKeyByLastActiveTimeCache<ThumbnailData> cache = new TaskKeyByLastActiveTimeCache<>(4);
+        Task.TaskKey key1 = new Task.TaskKey(1, 0, new Intent(),
+                new ComponentName("", ""), 0, 200);
+        cache.put(key1, new ThumbnailData());
+
+        Task.TaskKey key2 = new Task.TaskKey(2, 0, new Intent(),
+                new ComponentName("", ""), 0, 100);
+        cache.put(key2, new ThumbnailData());
+
+        Task.TaskKey key3 = new Task.TaskKey(3, 0, new Intent(),
+                new ComponentName("", ""), 0, 400);
+        cache.put(key3, new ThumbnailData());
+
+        Task.TaskKey key4 = new Task.TaskKey(4, 0, new Intent(),
+                new ComponentName("", ""), 0, 300);
+        cache.put(key4, new ThumbnailData());
+
+        // Check that it has 4 entries before cache size changes
+        assertEquals(4, cache.getSize());
+        assertEquals(4, cache.getQueue().size());
+
+        // Update size to 2
+        cache.updateCacheSizeAndRemoveExcess(2);
+
+        // Number of entries becomes 2, only key3 and key4 remain
+        assertEquals(2, cache.getSize());
+        assertEquals(2, cache.getQueue().size());
+        assertNotNull(cache.getAndInvalidateIfModified(key3));
+        assertNotNull(cache.getAndInvalidateIfModified(key4));
+    }
+}
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index 32e3b77..276d73e 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -37,11 +37,14 @@
         android:id="@+id/pause_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:maxWidth="@dimen/work_fab_width"
         android:textColor="@color/work_fab_icon_color"
         android:textSize="14sp"
         android:includeFontPadding="false"
         android:textDirection="locale"
         android:text="@string/work_apps_pause_btn_text"
         android:layout_marginStart="@dimen/work_fab_text_start_margin"
+        android:ellipsize="end"
+        android:maxLines="1"
         style="@style/TextHeadline"/>
 </com.android.launcher3.allapps.WorkModeSwitch>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index f6a94cd..35cac5b 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -64,7 +64,7 @@
     <string name="notifications_header" msgid="1404149926117359025">"Ειδοποιήσεις"</string>
     <string name="long_press_shortcut_to_add" msgid="5405328730817637737">"Πατήστε παρατεταμένα για μετακίνηση συντόμευσης."</string>
     <string name="long_accessible_way_to_add_shortcut" msgid="2199537273817090740">"Πατήστε δύο φορές παρατεταμένα για μετακίνηση συντόμευσης ή χρήση προσαρμοσμένων ενεργειών."</string>
-    <string name="out_of_space" msgid="6455557115204099579">"Δεν υπάρχει χώρος σε αυτήν την αρχική οθόνη"</string>
+    <string name="out_of_space" msgid="6455557115204099579">"Δεν υπάρχει χώρος σε αυτή την αρχική οθόνη"</string>
     <string name="hotseat_out_of_space" msgid="7448809638125333693">"Δεν υπάρχει επιπλέον χώρος στην περιοχή Αγαπημένα"</string>
     <string name="all_apps_button_label" msgid="8130441508702294465">"Λίστα εφαρμογών"</string>
     <string name="all_apps_search_results" msgid="5889367432531296759">"Αποτελέσματα αναζήτησης"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 7c718e59..8301194 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -35,7 +35,7 @@
     <string name="widget_dims_format" msgid="2370757736025621599">"%1$d × %2$d"</string>
     <string name="widget_accessible_dims_format" msgid="3640149169885301790">"%1$d चौड़ाई गुणा %2$d ऊंचाई"</string>
     <string name="widget_preview_context_description" msgid="9045841361655787574">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट"</string>
-    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"होम स्क्रीन पर इधर-उधर ले जाने के लिए, विजेट को दबाकर रखें"</string>
+    <string name="add_item_request_drag_hint" msgid="8730547755622776606">"विजेट को होम स्क्रीन पर इधर-उधर ले जाने के लिए, उसे दबाकर रखें"</string>
     <string name="add_to_home_screen" msgid="9168649446635919791">"होम स्क्रीन पर जोड़ें"</string>
     <string name="added_to_home_screen_accessibility_text" msgid="4451545765448884415">"<xliff:g id="WIDGET_NAME">%1$s</xliff:g> विजेट को होम स्क्रीन पर जोड़ा गया"</string>
     <string name="suggested_widgets_header_title" msgid="1844314680798145222">"सुझाव"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index d3841a3..555913e 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -104,7 +104,7 @@
     <string name="settings_button_text" msgid="8873672322605444408">"ହୋମ ସେଟିଂସ"</string>
     <string name="msg_disabled_by_admin" msgid="6898038085516271325">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ଅକ୍ଷମ କରାଯାଇଛି"</string>
     <string name="allow_rotation_title" msgid="7222049633713050106">"ହୋମ ସ୍କ୍ରିନ ରୋଟେସନକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
-    <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ବୁଲାଯାଇଥାଏ"</string>
+    <string name="allow_rotation_desc" msgid="8662546029078692509">"ଯେତେବେଳେ ଫୋନକୁ ରୋଟେଟ କରାଯାଇଥାଏ"</string>
     <string name="notification_dots_title" msgid="9062440428204120317">"ବିଜ୍ଞପ୍ତି ଡଟ୍ସ"</string>
     <string name="notification_dots_desc_on" msgid="1679848116452218908">"ଚାଲୁ"</string>
     <string name="notification_dots_desc_off" msgid="1760796511504341095">"ବନ୍ଦ କରନ୍ତୁ"</string>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c6fce28..10f47cb 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -141,6 +141,7 @@
     <dimen name="work_fab_icon_size">24dp</dimen>
     <dimen name="work_fab_text_start_margin">8dp</dimen>
     <dimen name="work_card_padding_horizontal">10dp</dimen>
+    <dimen name="work_fab_width">214dp</dimen>
     <dimen name="work_card_button_height">52dp</dimen>
     <dimen name="work_fab_margin">16dp</dimen>
     <dimen name="work_fab_margin_bottom">20dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1b46b4d..a2f4a61 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -433,7 +433,7 @@
     <!-- button string shown to dismiss work tab education -->
     <string name="work_apps_paused_edu_accept">Got it</string>
 
-    <!-- button string shown pause work profile -->
+    <!-- button string shown pause work profile [CHAR LIMIT=28] -->
     <string name="work_apps_pause_btn_text">Pause work apps</string>
     <!-- button string shown enable work profile -->
     <string name="work_apps_enable_btn_text">Unpause</string>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index bd004e9..ab9836f 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import static android.text.Layout.Alignment.ALIGN_NORMAL;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
 import static com.android.launcher3.config.FeatureFlags.enableCursorHoverStates;
@@ -39,6 +40,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.icu.text.MessageFormat;
+import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
@@ -148,6 +150,7 @@
     private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
     private final ActivityContext mActivity;
     private FastBitmapDrawable mIcon;
+    private DeviceProfile mDeviceProfile;
     private boolean mCenterVertically;
 
     protected int mDisplay;
@@ -206,35 +209,35 @@
         mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
         mIsRtl = (getResources().getConfiguration().getLayoutDirection()
                 == View.LAYOUT_DIRECTION_RTL);
-        DeviceProfile grid = mActivity.getDeviceProfile();
+        mDeviceProfile = mActivity.getDeviceProfile();
 
         mDisplay = a.getInteger(R.styleable.BubbleTextView_iconDisplay, DISPLAY_WORKSPACE);
         final int defaultIconSize;
         if (mDisplay == DISPLAY_WORKSPACE) {
-            setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
-            setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
-            defaultIconSize = grid.iconSizePx;
-            setCenterVertically(grid.iconCenterVertically);
+            setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.iconTextSizePx);
+            setCompoundDrawablePadding(mDeviceProfile.iconDrawablePaddingPx);
+            defaultIconSize = mDeviceProfile.iconSizePx;
+            setCenterVertically(mDeviceProfile.iconCenterVertically);
         } else if (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW
                 || mDisplay == DISPLAY_SEARCH_RESULT_APP_ROW) {
-            setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
-            setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
-            defaultIconSize = grid.allAppsIconSizePx;
+            setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx);
+            setCompoundDrawablePadding(mDeviceProfile.allAppsIconDrawablePaddingPx);
+            defaultIconSize = mDeviceProfile.allAppsIconSizePx;
         } else if (mDisplay == DISPLAY_FOLDER) {
-            setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.folderChildTextSizePx);
-            setCompoundDrawablePadding(grid.folderChildDrawablePaddingPx);
-            defaultIconSize = grid.folderChildIconSizePx;
+            setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.folderChildTextSizePx);
+            setCompoundDrawablePadding(mDeviceProfile.folderChildDrawablePaddingPx);
+            defaultIconSize = mDeviceProfile.folderChildIconSizePx;
         } else if (mDisplay == DISPLAY_SEARCH_RESULT) {
-            setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
+            setTextSize(TypedValue.COMPLEX_UNIT_PX, mDeviceProfile.allAppsIconTextSizePx);
             defaultIconSize = getResources().getDimensionPixelSize(R.dimen.search_row_icon_size);
         } else if (mDisplay == DISPLAY_SEARCH_RESULT_SMALL) {
             defaultIconSize = getResources().getDimensionPixelSize(
                     R.dimen.search_row_small_icon_size);
         } else if (mDisplay == DISPLAY_TASKBAR) {
-            defaultIconSize = grid.iconSizePx;
+            defaultIconSize = mDeviceProfile.iconSizePx;
         } else {
             // widget_selection or shortcut_popup
-            defaultIconSize = grid.iconSizePx;
+            defaultIconSize = mDeviceProfile.iconSizePx;
         }
 
         mCenterVertically = a.getBoolean(R.styleable.BubbleTextView_centerVertically, false);
@@ -274,8 +277,7 @@
         mDotParams.scale = 0f;
         mForceHideDot = false;
         setBackground(null);
-        if (Flags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()
-                || FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()) {
+        if (FeatureFlags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()) {
             setMaxLines(1);
         }
 
@@ -407,8 +409,8 @@
      *  Only if actual text can be displayed in two line, the {@code true} value will be effective.
      */
     protected boolean shouldUseTwoLine() {
-        return ((Flags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get())
-                && mDisplay == DISPLAY_ALL_APPS)
+        return ((FeatureFlags.enableTwolineAllapps())
+                && (mDisplay == DISPLAY_ALL_APPS || mDisplay == DISPLAY_PREDICTION_ROW))
                 || (FeatureFlags.ENABLE_TWOLINE_DEVICESEARCH.get()
                 && mDisplay == DISPLAY_SEARCH_RESULT);
     }
@@ -691,21 +693,28 @@
 
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = MeasureSpec.getSize(heightMeasureSpec);
         if (mCenterVertically) {
             Paint.FontMetrics fm = getPaint().getFontMetrics();
             int cellHeightPx = mIconSize + getCompoundDrawablePadding() +
                     (int) Math.ceil(fm.bottom - fm.top);
-            int height = MeasureSpec.getSize(heightMeasureSpec);
             setPadding(getPaddingLeft(), (height - cellHeightPx) / 2, getPaddingRight(),
                     getPaddingBottom());
         }
         // Only apply two line for all_apps and device search only if necessary.
         if (shouldUseTwoLine() && (mLastOriginalText != null)) {
+            int allowedVerticalSpace = height - getPaddingTop() - getPaddingBottom()
+                    - mDeviceProfile.allAppsIconSizePx
+                    - mDeviceProfile.allAppsIconDrawablePaddingPx;
             CharSequence modifiedString = modifyTitleToSupportMultiLine(
                     MeasureSpec.getSize(widthMeasureSpec) - getCompoundPaddingLeft()
                             - getCompoundPaddingRight(),
+                    allowedVerticalSpace,
                     mLastOriginalText,
-                    getPaint(), mBreakPointsIntArray);
+                    getPaint(),
+                    mBreakPointsIntArray,
+                    getLineSpacingMultiplier(),
+                    getLineSpacingExtra());
             if (!TextUtils.equals(modifiedString, mLastModifiedText)) {
                 mLastModifiedText = modifiedString;
                 setText(modifiedString);
@@ -785,7 +794,8 @@
      * many words as it can until the limit is reached. Once the limit is reached, we decide to
      * either return the original title or continue on a new line. How to get the new string is by
      * iterating through the list of break points and determining if the strings between the break
-     * points can fit within the line it is in.
+     * points can fit within the line it is in. We will show the modified string if there is enough
+     * horizontal and vertical space, otherwise this method will just return the original string.
      *  Example assuming each character takes up one spot:
      *  title = "Battery Stats", breakpoint = [6], stringPtr = 0, limitedWidth = 7
      *  We get the current word -> from sublist(0, breakpoint[i]+1) so sublist (0,7) -> Battery,
@@ -794,8 +804,9 @@
      *  if the first char is a SPACE, we trim to append "Stats". So resulting string would be
      *  "Battery\nStats"
      */
-    public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, CharSequence title,
-            TextPaint paint, IntArray breakPoints) {
+    public static CharSequence modifyTitleToSupportMultiLine(int limitedWidth, int limitedHeight,
+            CharSequence title, TextPaint paint, IntArray breakPoints, float spacingMultiplier,
+            float spacingExtra) {
         // current title is less than the width allowed so we can just skip
         if (title == null || paint.measureText(title, 0, title.length()) <= limitedWidth) {
             return title;
@@ -816,11 +827,7 @@
             if (runningWidth <= limitedWidth) {
                 newString.append(currentWord);
             } else {
-                // there is no more space
-                if (i == 0) {
-                    // if the first words exceeds width, just return as the first line will ellipse
-                    return title;
-                } else {
+                if (i != 0)  {
                     // If putting word onto a new line, make sure there is no space or new line
                     // character in the beginning of the current word and just put in the rest of
                     // the characters.
@@ -834,8 +841,14 @@
                                 : EMPTY;
                     }
                     newString.append(NEW_LINE).append(lastCharacters);
-                    return newString.toString();
+                    StaticLayout staticLayout = new StaticLayout(newString, paint, limitedWidth,
+                            ALIGN_NORMAL, spacingMultiplier, spacingExtra, false);
+                    if (staticLayout.getHeight() < limitedHeight) {
+                        return newString.toString();
+                    }
                 }
+                // if the first words exceeds width, just return as the first line will ellipse
+                return title;
             }
             if (i >= breakPoints.size()) {
                 // no need to look forward into the string if we've already finished processing
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index c05afc9..d8804a1 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -300,7 +300,6 @@
     // If true, used to layout taskbar in 3 button navigation mode.
     public final boolean startAlignTaskbar;
     public final boolean isTransientTaskbar;
-
     // DragController
     public int flingToDeleteThresholdVelocity;
 
@@ -309,7 +308,8 @@
             SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
             boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
             @NonNull final ViewScaleProvider viewScaleProvider,
-            @NonNull final Consumer<DeviceProfile> dimensionOverrideProvider) {
+            @NonNull final Consumer<DeviceProfile> dimensionOverrideProvider,
+            boolean isTransientTaskbar) {
 
         this.inv = inv;
         this.isLandscape = windowBounds.isLandscape();
@@ -367,7 +367,7 @@
             }
         }
 
-        isTransientTaskbar = DisplayController.isTransientTaskbar(context);
+        this.isTransientTaskbar = isTransientTaskbar;
         if (!isTaskbarPresent) {
             taskbarIconSize = taskbarHeight = stashedTaskbarHeight = taskbarBottomMargin = 0;
             startAlignTaskbar = false;
@@ -1133,6 +1133,10 @@
         if (isVerticalBarLayout()) {
             hideWorkspaceLabelsIfNotEnoughSpace();
         }
+        if (FeatureFlags.enableTwolineAllapps()) {
+            // Add extra textHeight to the existing allAppsCellHeight.
+            allAppsCellHeightPx += Utilities.calculateTextHeight(allAppsIconTextSizePx);
+        }
 
         updateHotseatSizes(iconSizePx);
 
@@ -2119,10 +2123,13 @@
 
         private Consumer<DeviceProfile> mOverrideProvider;
 
+        private boolean mIsTransientTaskbar;
+
         public Builder(Context context, InvariantDeviceProfile inv, Info info) {
             mContext = context;
             mInv = inv;
             mInfo = info;
+            mIsTransientTaskbar = info.isTransientTaskbar();
         }
 
         public Builder setMultiWindowMode(boolean isMultiWindowMode) {
@@ -2173,6 +2180,15 @@
             return this;
         }
 
+        /**
+         * Set the isTransientTaskbar for the builder
+         * @return This Builder
+         */
+        public Builder setIsTransientTaskbar(boolean isTransientTaskbar) {
+            mIsTransientTaskbar = isTransientTaskbar;
+            return this;
+        }
+
         public DeviceProfile build() {
             if (mWindowBounds == null) {
                 throw new IllegalArgumentException("Window bounds not set");
@@ -2194,7 +2210,7 @@
             }
             return new DeviceProfile(mContext, mInv, mInfo, mWindowBounds, mDotRendererCache,
                     mIsMultiWindowMode, mTransposeLayoutWithOrientation, mIsMultiDisplay,
-                    mIsGestureMode, mViewScaleProvider, mOverrideProvider);
+                    mIsGestureMode, mViewScaleProvider, mOverrideProvider, mIsTransientTaskbar);
         }
     }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index c619bc5..04e8da1 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -22,6 +22,7 @@
 import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
 import static com.android.launcher3.util.DisplayController.CHANGE_NAVIGATION_MODE;
 import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
+import static com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
 import android.annotation.TargetApi;
@@ -224,7 +225,7 @@
         DisplayController.INSTANCE.get(context).setPriorityListener(
                 (displayContext, info, flags) -> {
                     if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS
-                            | CHANGE_NAVIGATION_MODE)) != 0) {
+                            | CHANGE_NAVIGATION_MODE | CHANGE_TASKBAR_PINNING)) != 0) {
                         onConfigChanged(displayContext);
                     }
                 });
@@ -888,7 +889,7 @@
             deviceCategory = a.getInt(R.styleable.GridDisplayOption_deviceCategory,
                     DEVICE_CATEGORY_ALL);
 
-            if (FeatureFlags.ENABLE_RESPONSIVE_WORKSPACE.get()) {
+            if (FeatureFlags.enableResponsiveWorkspace()) {
                 mWorkspaceSpecsId = a.getResourceId(
                         R.styleable.GridDisplayOption_workspaceSpecsId, INVALID_RESOURCE_HANDLE);
                 mWorkspaceSpecsTwoPanelId = a.getResourceId(
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b8e7737..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
@@ -427,6 +426,10 @@
 
     private final List<BackPressHandler> mBackPressedHandlers = new ArrayList<>();
 
+    public static Launcher getLauncher(Context context) {
+        return fromContext(context);
+    }
+
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
@@ -672,10 +675,6 @@
         return new OnboardingPrefs<>(this, sharedPrefs);
     }
 
-    public OnboardingPrefs<? extends Launcher> getOnboardingPrefs() {
-        return mOnboardingPrefs;
-    }
-
     @Override
     public void onPluginConnected(LauncherOverlayPlugin overlayManager, Context context) {
         switchOverlay(() -> overlayManager.createOverlayManager(this, this));
@@ -789,56 +788,8 @@
         return true;
     }
 
-    @Override
-    public CellPosMapper getCellPosMapper() {
-        return mCellPosMapper;
-    }
-
-    public RotationHelper getRotationHelper() {
-        return mRotationHelper;
-    }
-
-    public ViewGroupFocusHelper getFocusHandler() {
-        return mFocusHandler;
-    }
-
-    @Override
-    public StateManager<LauncherState> getStateManager() {
-        return mStateManager;
-    }
-
     private LauncherCallbacks mLauncherCallbacks;
 
-    /**
-     * Call this after onCreate to set or clear overlay.
-     */
-    @Override
-    public void setLauncherOverlay(LauncherOverlay overlay) {
-        mWorkspace.setLauncherOverlay(overlay);
-    }
-
-    public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
-        mLauncherCallbacks = callbacks;
-        return true;
-    }
-
-    public boolean isDraggingEnabled() {
-        // We prevent dragging when we are loading the workspace as it is possible to pick up a view
-        // that is subsequently removed from the workspace in startBinding().
-        return !isWorkspaceLoading();
-    }
-
-    @NonNull
-    @Override
-    public PopupDataProvider getPopupDataProvider() {
-        return mPopupDataProvider;
-    }
-
-    @Override
-    public DotInfo getDotInfoForItem(ItemInfo info) {
-        return mPopupDataProvider.getDotInfoForItem(info);
-    }
-
     @Override
     public void invalidateParent(ItemInfo info) {
         if (info.container >= 0) {
@@ -1178,10 +1129,6 @@
         mDeferOverlayCallbacks = true;
     }
 
-    public LauncherOverlayManager getOverlayManager() {
-        return mOverlayManager;
-    }
-
     @Override
     public void onStateSetStart(LauncherState state) {
         super.onStateSetStart(state);
@@ -1589,79 +1536,11 @@
         return instance;
     }
 
-    public AllAppsTransitionController getAllAppsController() {
-        return mAllAppsController;
-    }
-
-    @Override
-    public DragLayer getDragLayer() {
-        return mDragLayer;
-    }
-
-    @Override
-    public ActivityAllAppsContainerView<Launcher> getAppsView() {
-        return mAppsView;
-    }
-
-    public Workspace<?> getWorkspace() {
-        return mWorkspace;
-    }
-
-    public Hotseat getHotseat() {
-        return mHotseat;
-    }
-
-    public <T extends View> T getOverviewPanel() {
-        return (T) mOverviewPanel;
-    }
-
-    public DropTargetBar getDropTargetBar() {
-        return mDropTargetBar;
-    }
-
-    @Override
-    public ScrimView getScrimView() {
-        return mScrimView;
-    }
-
-    public LauncherWidgetHolder getAppWidgetHolder() {
-        return mAppWidgetHolder;
-    }
-
     protected LauncherWidgetHolder createAppWidgetHolder() {
         return LauncherWidgetHolder.HolderFactory.newFactory(this).newInstance(
                 this, appWidgetId -> getWorkspace().removeWidget(appWidgetId));
     }
 
-    public LauncherModel getModel() {
-        return mModel;
-    }
-
-    /**
-     * Returns the ModelWriter writer, make sure to call the function every time you want to use it.
-     */
-    public ModelWriter getModelWriter() {
-        return mModelWriter;
-    }
-
-    @Override
-    public SharedPreferences getSharedPrefs() {
-        return mSharedPrefs;
-    }
-
-    @Override
-    public SharedPreferences getDevicePrefs() {
-        return LauncherPrefs.getDevicePrefs(this);
-    }
-
-    public int getOrientation() {
-        return mOldConfig.orientation;
-    }
-
-    public BaseSearchConfig getSearchConfig() {
-        return mBaseSearchConfig;
-    }
-
     @Override
     protected void onNewIntent(Intent intent) {
         if (Utilities.isRunningInTestHarness()) {
@@ -1895,27 +1774,6 @@
         mStateManager.goToState(NORMAL);
     }
 
-    public boolean isWorkspaceLocked() {
-        return mWorkspaceLoading || mPendingRequestArgs != null;
-    }
-
-    public boolean isWorkspaceLoading() {
-        return mWorkspaceLoading;
-    }
-
-    @Override
-    public boolean isBindingItems() {
-        return mWorkspaceLoading;
-    }
-
-    private void setWorkspaceLoading(boolean value) {
-        mWorkspaceLoading = value;
-    }
-
-    public void setWaitingForResult(PendingRequestArgs args) {
-        mPendingRequestArgs = args;
-    }
-
     void addAppWidgetFromDropImpl(int appWidgetId, ItemInfo info, AppWidgetHostView boundWidget,
             WidgetAddFlowHandler addFlowHandler) {
         if (LOGD) {
@@ -2145,13 +2003,6 @@
         return super.dispatchTouchEvent(ev);
     }
 
-    /**
-     * Returns true if a touch interaction is in progress
-     */
-    public boolean isTouchInProgress() {
-        return mTouchInProgress;
-    }
-
     @Override
     @TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void onBackPressed() {
@@ -2213,16 +2064,6 @@
         return mHotseat != null && (layout == mHotseat);
     }
 
-    /**
-     * Returns the CellLayout of the specified container at the specified screen.
-     *
-     * @param screenId must be presenterPos and not modelPos.
-     */
-    public CellLayout getCellLayout(int container, int screenId) {
-        return (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
-                ? mHotseat : mWorkspace.getScreenWithId(screenId);
-    }
-
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
@@ -2249,22 +2090,6 @@
         return result;
     }
 
-    /**
-     * Persistant callback which notifies when an activity launch is deferred because the activity
-     * was not yet resumed.
-     */
-    public void setOnDeferredActivityLaunchCallback(Runnable callback) {
-        mOnDeferredActivityLaunchCallback = callback;
-    }
-
-    /**
-     * Sets the next pages to bind synchronously on next bind.
-     * @param pages should not be null.
-     */
-    public void setPagesToBindSynchronously(@NonNull IntSet pages) {
-        mPagesToBindSynchronously = pages;
-    }
-
     @Override
     public IntSet getPagesToBindSynchronously(IntArray orderedScreenIds) {
         IntSet visibleIds;
@@ -3110,11 +2935,6 @@
         mAppsView.updateWorkUI();
     }
 
-    @Override
-    public StringCache getStringCache() {
-        return mStringCache;
-    }
-
     /**
      * @param packageUser if null, refreshes all widgets and shortcuts, otherwise only
      *                    refreshes the widgets and shortcuts associated with the given package/user
@@ -3183,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);
     }
 
     /**
@@ -3271,18 +3058,6 @@
                 false);
     }
 
-    /**
-     * Returns target rectangle for anchoring a popup menu.
-     */
-    protected RectF getPopupTarget(float x, float y) {
-        float halfSize = getResources().getDimension(R.dimen.options_menu_thumb_size) / 2;
-        if (x < 0 || y < 0) {
-            x = mDragLayer.getWidth() / 2;
-            y = mDragLayer.getHeight() / 2;
-        }
-        return new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
-    }
-
     @Override
     public boolean canUseMultipleShadesForPopup() {
         return getTopOpenViewWithType(this, TYPE_FOLDER) == null
@@ -3338,7 +3113,7 @@
         getStateManager().goToState(LauncherState.NORMAL);
     }
 
-    private void closeOpenViews() {
+    public void closeOpenViews() {
         closeOpenViews(true);
     }
 
@@ -3346,10 +3121,6 @@
         AbstractFloatingView.closeAllOpenViews(this, animate);
     }
 
-    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
-        return Stream.of(APP_INFO, WIDGETS, INSTALL);
-    }
-
     protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
         return new LauncherAccessibilityDelegate(this);
     }
@@ -3358,16 +3129,6 @@
     @VisibleForTesting
     public void enableHotseatEdu(boolean enable) {}
 
-    /**
-     * @see LauncherState#getOverviewScaleAndOffset(Launcher)
-     */
-    public float[] getNormalOverviewScaleAndOffset() {
-        return new float[] {NO_SCALE, NO_OFFSET};
-    }
-
-    public static Launcher getLauncher(Context context) {
-        return fromContext(context);
-    }
 
     /**
      * Just a wrapper around the type cast to allow easier tracking of calls.
@@ -3405,20 +3166,6 @@
         public Configuration config;
     }
 
-    @Override
-    public StatsLogManager getStatsLogManager() {
-        return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
-    }
-
-    /**
-     * Returns the current popup for testing, if any.
-     */
-    @VisibleForTesting
-    @Nullable
-    public ArrowPopup<?> getOptionsPopup() {
-        return findViewById(R.id.popup_container);
-    }
-
     /** Pauses view updates that should not be run during the app launch animation. */
     public void pauseExpensiveViewUpdates() {
         // Pause page indicator animations as they lead to layer trashing.
@@ -3452,9 +3199,212 @@
         return false; // Base launcher does not track freeform tasks
     }
 
+    // Getters and Setters
+
+    private void setWorkspaceLoading(boolean value) {
+        mWorkspaceLoading = value;
+    }
+
+    public boolean isWorkspaceLocked() {
+        return mWorkspaceLoading || mPendingRequestArgs != null;
+    }
+
+    public boolean isWorkspaceLoading() {
+        return mWorkspaceLoading;
+    }
+
     @Override
-    public View.OnLongClickListener getAllAppsItemLongClickListener() {
-        return ItemLongClickListener.INSTANCE_ALL_APPS;
+    public boolean isBindingItems() {
+        return mWorkspaceLoading;
+    }
+
+    /**
+     * Returns true if a touch interaction is in progress
+     */
+    public boolean isTouchInProgress() {
+        return mTouchInProgress;
+    }
+
+    public boolean isDraggingEnabled() {
+        // We prevent dragging when we are loading the workspace as it is possible to pick up a view
+        // that is subsequently removed from the workspace in startBinding().
+        return !isWorkspaceLoading();
+    }
+
+    public void setWaitingForResult(PendingRequestArgs args) {
+        mPendingRequestArgs = args;
+    }
+
+    /**
+     * Call this after onCreate to set or clear overlay.
+     */
+    @Override
+    public void setLauncherOverlay(LauncherOverlay overlay) {
+        mWorkspace.setLauncherOverlay(overlay);
+    }
+
+    public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
+        mLauncherCallbacks = callbacks;
+        return true;
+    }
+
+    /**
+     * Persistent callback which notifies when an activity launch is deferred because the activity
+     * was not yet resumed.
+     */
+    public void setOnDeferredActivityLaunchCallback(Runnable callback) {
+        mOnDeferredActivityLaunchCallback = callback;
+    }
+
+    /**
+     * Sets the next pages to bind synchronously on next bind.
+     * @param pages should not be null.
+     */
+    public void setPagesToBindSynchronously(@NonNull IntSet pages) {
+        mPagesToBindSynchronously = pages;
+    }
+
+    public OnboardingPrefs<? extends Launcher> getOnboardingPrefs() {
+        return mOnboardingPrefs;
+    }
+
+    @Override
+    public CellPosMapper getCellPosMapper() {
+        return mCellPosMapper;
+    }
+
+    public RotationHelper getRotationHelper() {
+        return mRotationHelper;
+    }
+
+    public ViewGroupFocusHelper getFocusHandler() {
+        return mFocusHandler;
+    }
+
+    @Override
+    public StateManager<LauncherState> getStateManager() {
+        return mStateManager;
+    }
+
+    @NonNull
+    @Override
+    public PopupDataProvider getPopupDataProvider() {
+        return mPopupDataProvider;
+    }
+
+    @Override
+    public DotInfo getDotInfoForItem(ItemInfo info) {
+        return mPopupDataProvider.getDotInfoForItem(info);
+    }
+
+    public LauncherOverlayManager getOverlayManager() {
+        return mOverlayManager;
+    }
+
+    public AllAppsTransitionController getAllAppsController() {
+        return mAllAppsController;
+    }
+
+    @Override
+    public DragLayer getDragLayer() {
+        return mDragLayer;
+    }
+
+    @Override
+    public ActivityAllAppsContainerView<Launcher> getAppsView() {
+        return mAppsView;
+    }
+
+    public Workspace<?> getWorkspace() {
+        return mWorkspace;
+    }
+
+    public Hotseat getHotseat() {
+        return mHotseat;
+    }
+
+    public <T extends View> T getOverviewPanel() {
+        return (T) mOverviewPanel;
+    }
+
+    public DropTargetBar getDropTargetBar() {
+        return mDropTargetBar;
+    }
+
+    @Override
+    public ScrimView getScrimView() {
+        return mScrimView;
+    }
+
+    public LauncherWidgetHolder getAppWidgetHolder() {
+        return mAppWidgetHolder;
+    }
+
+    public LauncherModel getModel() {
+        return mModel;
+    }
+
+    /**
+     * Returns the ModelWriter writer, make sure to call the function every time you want to use it.
+     */
+    public ModelWriter getModelWriter() {
+        return mModelWriter;
+    }
+
+    @Override
+    public SharedPreferences getSharedPrefs() {
+        return mSharedPrefs;
+    }
+
+    @Override
+    public SharedPreferences getDevicePrefs() {
+        return LauncherPrefs.getDevicePrefs(this);
+    }
+
+    public int getOrientation() {
+        return mOldConfig.orientation;
+    }
+
+    public BaseSearchConfig getSearchConfig() {
+        return mBaseSearchConfig;
+    }
+
+    /**
+     * Returns the CellLayout of the specified container at the specified screen.
+     *
+     * @param screenId must be presenterPos and not modelPos.
+     */
+    public CellLayout getCellLayout(int container, int screenId) {
+        return (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT)
+                ? mHotseat : mWorkspace.getScreenWithId(screenId);
+    }
+
+    @Override
+    public StringCache getStringCache() {
+        return mStringCache;
+    }
+
+    /**
+     * Returns target rectangle for anchoring a popup menu.
+     */
+    protected RectF getPopupTarget(float x, float y) {
+        float halfSize = getResources().getDimension(R.dimen.options_menu_thumb_size) / 2;
+        if (x < 0 || y < 0) {
+            x = mDragLayer.getWidth() / 2;
+            y = mDragLayer.getHeight() / 2;
+        }
+        return new RectF(x - halfSize, y - halfSize, x + halfSize, y + halfSize);
+    }
+
+    public Stream<SystemShortcut.Factory> getSupportedShortcuts() {
+        return Stream.of(APP_INFO, WIDGETS, INSTALL);
+    }
+
+    /**
+     * @see LauncherState#getOverviewScaleAndOffset(Launcher)
+     */
+    public float[] getNormalOverviewScaleAndOffset() {
+        return new float[] {NO_SCALE, NO_OFFSET};
     }
 
     /**
@@ -3471,4 +3421,25 @@
     public CannedAnimationCoordinator getAnimationCoordinator() {
         return mAnimationCoordinator;
     }
+
+    @Override
+    public View.OnLongClickListener getAllAppsItemLongClickListener() {
+        return ItemLongClickListener.INSTANCE_ALL_APPS;
+    }
+
+    @Override
+    public StatsLogManager getStatsLogManager() {
+        return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
+    }
+
+    /**
+     * Returns the current popup for testing, if any.
+     */
+    @VisibleForTesting
+    @Nullable
+    public ArrowPopup<?> getOptionsPopup() {
+        return findViewById(R.id.popup_container);
+    }
+
+    // End of Getters and Setters
 }
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index 36d37c7..e8d5116 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -59,8 +59,12 @@
                 IS_STARTUP_DATA_MIGRATED.defaultValue
             )
 
+    // TODO: Remove `item == TASKBAR_PINNING` once isBootAwareStartupDataEnabled is always true
     private fun chooseSharedPreferences(item: Item): SharedPreferences =
-        if (isBootAwareStartupDataEnabled && item.isBootAware && isStartupDataMigrated)
+        if (
+            (isBootAwareStartupDataEnabled && item.isBootAware && isStartupDataMigrated) ||
+                item == TASKBAR_PINNING
+        )
             bootAwarePrefs
         else item.encryptedPrefs
 
@@ -283,7 +287,7 @@
         @JvmField val WORK_EDU_STEP = backedUpItem(WorkProfileManager.KEY_WORK_EDU_STEP, 0)
         @JvmField val WORKSPACE_SIZE = backedUpItem(DeviceGridState.KEY_WORKSPACE_SIZE, "", true)
         @JvmField val HOTSEAT_COUNT = backedUpItem(DeviceGridState.KEY_HOTSEAT_COUNT, -1, true)
-        @JvmField val TASKBAR_PINNING = backedUpItem(TASKBAR_PINNING_KEY, false)
+        @JvmField val TASKBAR_PINNING = backedUpItem(TASKBAR_PINNING_KEY, false, true)
 
         @JvmField
         val DEVICE_TYPE =
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 9c4ce46..dabe84d 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -131,7 +131,7 @@
     protected final List<AdapterHolder> mAH;
     protected final Predicate<ItemInfo> mPersonalMatcher = ItemInfoMatcher.ofUser(
             Process.myUserHandle());
-    protected final WorkProfileManager mWorkManager;
+    protected WorkProfileManager mWorkManager;
     protected final Point mFastScrollerOffset = new Point();
     protected final int mScrimColor;
     protected final float mHeaderThreshold;
@@ -966,13 +966,14 @@
         mBottomSheetAlpha = mActivityContext.getDeviceProfile().isTablet ? 1f : alpha;
     }
 
-    private void onAppsUpdated() {
-        mHasWorkApps = Stream.of(mAllAppsStore.getApps()).anyMatch(mWorkManager.getMatcher());
+    @VisibleForTesting
+    public void onAppsUpdated() {
+        mHasWorkApps = mWorkManager.hasWorkApps();
         if (!isSearching()) {
             rebindAdapters();
-            if (mHasWorkApps) {
-                mWorkManager.reset();
-            }
+        }
+        if (mHasWorkApps) {
+            mWorkManager.reset();
         }
 
         mActivityContext.getStatsLogManager().logger()
@@ -1217,6 +1218,11 @@
     }
 
     @VisibleForTesting
+    public void setWorkManager(WorkProfileManager workManager) {
+        mWorkManager = workManager;
+    }
+
+    @VisibleForTesting
     public boolean isPersonalTabVisible() {
         return isDescendantViewVisible(R.id.tab_personal);
     }
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/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 769c787..005e6df 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -29,7 +29,6 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Flags;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
@@ -138,7 +137,6 @@
     protected final OnClickListener mOnIconClickListener;
     protected final OnLongClickListener mOnIconLongClickListener;
     protected OnFocusChangeListener mIconFocusListener;
-    private final int mExtraTextHeight;
 
     public BaseAllAppsAdapter(T activityContext, LayoutInflater inflater,
             AlphabeticalAppsList<T> apps, SearchAdapterProvider<?> adapterProvider) {
@@ -150,8 +148,6 @@
         mOnIconLongClickListener = mActivityContext.getAllAppsItemLongClickListener();
 
         mAdapterProvider = adapterProvider;
-        mExtraTextHeight = Utilities.calculateTextHeight(
-                mActivityContext.getDeviceProfile().allAppsIconTextSizePx);
     }
 
     /** Checks if the passed viewType represents all apps divider. */
@@ -177,9 +173,7 @@
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
             case VIEW_TYPE_ICON:
-                int layout =
-                        !(Flags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get())
-                                ? R.layout.all_apps_icon
+                int layout = !FeatureFlags.enableTwolineAllapps() ? R.layout.all_apps_icon
                                 : R.layout.all_apps_icon_twoline;
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         layout, parent, false);
@@ -190,9 +184,6 @@
                 // Ensure the all apps icon height matches the workspace icons in portrait mode.
                 icon.getLayoutParams().height =
                         mActivityContext.getDeviceProfile().allAppsCellHeightPx;
-                if (Flags.enableTwolineAllapps() || FeatureFlags.ENABLE_TWOLINE_ALLAPPS.get()) {
-                    icon.getLayoutParams().height += mExtraTextHeight;
-                }
                 return new ViewHolder(icon);
             case VIEW_TYPE_EMPTY_SEARCH:
                 return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 1ac8d87..05ed9ba 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -52,6 +52,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.function.Predicate;
+import java.util.stream.Stream;
 
 /**
  * Companion class for {@link ActivityAllAppsContainerView} to manage work tab and personal tab
@@ -215,6 +216,10 @@
         return mCurrentState != WorkProfileManager.STATE_DISABLED;
     }
 
+    public boolean hasWorkApps() {
+        return Stream.of(mAllApps.getAppsStore().getApps()).anyMatch(mMatcher);
+    }
+
     /**
      * Adds work profile specific adapter items to adapterItems and returns number of items added
      */
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 8b8f50a..9fb175d 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -35,13 +35,7 @@
 
 /**
  * Defines a set of flags used to control various launcher behaviors.
- *
- * Please only add flags to your assigned block to prevent merge conflicts. If you do not have
- * a block, please update the current empty block and add a new empty block below to prevent
- * merge conflicts with the previous block.
- * List of blocks can be found:
- * <a href="http://go/gnl-flags-block-directory">here</a>
- *
+ * <p>
  * <p>All the flags should be defined here with appropriate default values.
  */
 public final class FeatureFlags {
@@ -172,6 +166,10 @@
             "LARGE_SCREEN_WIDGET_PICKER", ENABLED, "Enable new widget picker that takes "
                     + "advantage of large screen format");
 
+    public static final BooleanFlag UNFOLDED_WIDGET_PICKER = getDebugFlag(301918659,
+            "UNFOLDED_WIDGET_PICKER", DISABLED, "Enable new widget picker that takes "
+                    + "advantage of the unfolded foldable format");
+
     public static final BooleanFlag MULTI_SELECT_EDIT_MODE = getDebugFlag(270709220,
             "MULTI_SELECT_EDIT_MODE", DISABLED, "Enable new multi-select edit mode "
                     + "for home screen");
@@ -192,6 +190,10 @@
             "ENABLE_PARAMETRIZE_REORDER", DISABLED,
             "Enables generating the reorder using a set of parameters");
 
+    public static final BooleanFlag ENABLE_NO_LONG_PRESS_DRAG = getDebugFlag(299748096,
+            "ENABLE_NO_LONG_PRESS_DRAG", DISABLED,
+            "Don't trigger the drag if we are still under long press");
+
     // TODO(Block 12): Clean up flags
     public static final BooleanFlag ENABLE_MULTI_INSTANCE = getDebugFlag(270396680,
             "ENABLE_MULTI_INSTANCE", DISABLED,
@@ -239,6 +241,9 @@
     // Aconfig migration complete for ENABLE_TWOLINE_ALLAPPS.
     public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
             "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps.");
+    public static boolean enableTwolineAllapps() {
+        return ENABLE_TWOLINE_ALLAPPS.get() || Flags.enableTwolineAllapps();
+    }
 
     public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693,
             "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps");
@@ -418,21 +423,32 @@
     // TODO(Block 31): Clean up flags
 
     // TODO(Block 32): Clean up flags
+    // Aconfig migration complete for ENABLE_RESPONSIVE_WORKSPACE.
+    @VisibleForTesting
     public static final BooleanFlag ENABLE_RESPONSIVE_WORKSPACE = getDebugFlag(241386436,
-            "ENABLE_RESPONSIVE_WORKSPACE", DISABLED,
+            "ENABLE_RESPONSIVE_WORKSPACE", TEAMFOOD,
             "Enables new workspace grid calculations method.");
+    public static boolean enableResponsiveWorkspace() {
+        return ENABLE_RESPONSIVE_WORKSPACE.get() || Flags.enableResponsiveWorkspace();
+    }
 
     // TODO(Block 33): Clean up flags
     public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
             "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED,
             "Enables preinflating all apps icons to avoid scrolling jank.");
-
-    // TODO(Block 34): Clean up flags
     public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
             "ALL_APPS_GONE_VISIBILITY", ENABLED,
             "Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
 
-    // TODO(Block 35): Empty block
+    // TODO(Block 34): Empty block
+    // Please only add flags to your assigned block. If you do not have a block:
+    // 1. Assign yourself this block
+    // 2. Add your flag to this block
+    // 3. Add a new empty block below this one
+    // 4. Move this comment to that new empty block
+    // This is all to prevent merge conflicts in the future and help keep track of who owns which
+    // flags.
+    // List of assigned blocks can be found: http://go/gnl-flags-block-directory
 
     public static class BooleanFlag {
 
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 0d51d48..777f4d5 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.dragndrop;
 
 import static com.android.launcher3.Utilities.ATLEAST_Q;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_NO_LONG_PRESS_DRAG;
 
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -87,6 +88,10 @@
     private int mLastTouchClassification;
     protected int mDistanceSinceScroll = 0;
 
+    /**
+     * This variable is to differentiate between a long press and a drag, if it's true that means
+     * it's a long press and when it's false means that we are no longer in a long press.
+     */
     protected boolean mIsInPreDrag;
 
     private final int DRAG_VIEW_SCALE_DURATION_MS = 500;
@@ -370,7 +375,7 @@
     @Override
     public void onDriverDragEnd(float x, float y) {
         if (!endWithFlingAnimation()) {
-            drop(findDropTarget((int) x, (int) y, mCoordinatesTemp), null);
+            drop(findDropTarget((int) x, (int) y), null);
         }
         endDrag();
     }
@@ -432,13 +437,6 @@
     protected void handleMoveEvent(int x, int y) {
         mDragObject.dragView.move(x, y);
 
-        // Drop on someone?
-        final int[] coordinates = mCoordinatesTemp;
-        DropTarget dropTarget = findDropTarget(x, y, coordinates);
-        mDragObject.x = coordinates[0];
-        mDragObject.y = coordinates[1];
-        checkTouchMove(dropTarget);
-
         // Check if we are hovering over the scroll areas
         mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y);
         mLastTouch.set(x, y);
@@ -451,6 +449,9 @@
                 && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) {
             callOnDragStart();
         }
+
+        // Drop on someone?
+        checkTouchMove(x, y);
     }
 
     public float getDistanceDragged() {
@@ -458,14 +459,15 @@
     }
 
     public void forceTouchMove() {
-        int[] placeholderCoordinates = mCoordinatesTemp;
-        DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, placeholderCoordinates);
-        mDragObject.x = placeholderCoordinates[0];
-        mDragObject.y = placeholderCoordinates[1];
-        checkTouchMove(dropTarget);
+        checkTouchMove(mLastTouch.x, mLastTouch.y);
     }
 
-    private void checkTouchMove(DropTarget dropTarget) {
+    private DropTarget checkTouchMove(final int x, final int y) {
+        // If we are in predrag, don't trigger any other event until we get out of it
+        if (ENABLE_NO_LONG_PRESS_DRAG.get() && mIsInPreDrag) {
+            return mLastDropTarget;
+        }
+        DropTarget dropTarget = findDropTarget(x, y);
         if (dropTarget != null) {
             if (mLastDropTarget != dropTarget) {
                 if (mLastDropTarget != null) {
@@ -474,12 +476,11 @@
                 dropTarget.onDragEnter(mDragObject);
             }
             dropTarget.onDragOver(mDragObject);
-        } else {
-            if (mLastDropTarget != null) {
-                mLastDropTarget.onDragExit(mDragObject);
-            }
+        } else if (mLastDropTarget != null) {
+            mLastDropTarget.onDragExit(mDragObject);
         }
         mLastDropTarget = dropTarget;
+        return mLastDropTarget;
     }
 
     /**
@@ -487,13 +488,8 @@
      * we manually ensure appropriate drag and drop events get emulated for accessible drag.
      */
     public void completeAccessibleDrag(int[] location) {
-        final int[] coordinates = mCoordinatesTemp;
-
         // We make sure that we prime the target for drop.
-        DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates);
-        mDragObject.x = coordinates[0];
-        mDragObject.y = coordinates[1];
-        checkTouchMove(dropTarget);
+        DropTarget dropTarget = checkTouchMove(location[0], location[1]);
 
         dropTarget.prepareAccessibilityDrop();
         // Perform the drop
@@ -502,10 +498,6 @@
     }
 
     protected void drop(DropTarget dropTarget, Runnable flingAnimation) {
-        final int[] coordinates = mCoordinatesTemp;
-        mDragObject.x = coordinates[0];
-        mDragObject.y = coordinates[1];
-
         // Move dragging to the final target.
         if (dropTarget != mLastDropTarget) {
             if (mLastDropTarget != null) {
@@ -542,9 +534,9 @@
         dispatchDropComplete(dropTargetAsView, accepted);
     }
 
-    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
-        mDragObject.x = x;
-        mDragObject.y = y;
+    private DropTarget findDropTarget(final int x, final int y) {
+        mCoordinatesTemp[0] = x;
+        mCoordinatesTemp[1] = y;
 
         final Rect r = mRectTemp;
         final ArrayList<DropTarget> dropTargets = mDropTargets;
@@ -556,17 +548,17 @@
 
             target.getHitRectRelativeToDragLayer(r);
             if (r.contains(x, y)) {
-                dropCoordinates[0] = x;
-                dropCoordinates[1] = y;
-                mActivity.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
+                mActivity.getDragLayer().mapCoordInSelfToDescendant((View) target,
+                        mCoordinatesTemp);
+                mDragObject.x = mCoordinatesTemp[0];
+                mDragObject.y = mCoordinatesTemp[1];
                 return target;
             }
         }
-        // Pass all unhandled drag to workspace. Workspace finds the correct
-        // cell layout to drop to in the existing drag/drop logic.
-        dropCoordinates[0] = x;
-        dropCoordinates[1] = y;
-        return getDefaultDropTarget(dropCoordinates);
+        DropTarget dropTarget = getDefaultDropTarget(mCoordinatesTemp);
+        mDragObject.x = mCoordinatesTemp[0];
+        mDragObject.y = mCoordinatesTemp[1];
+        return dropTarget;
     }
 
     protected abstract DropTarget getDefaultDropTarget(int[] dropCoordinates);
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 7bcd038..5e88b9b 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -170,11 +170,15 @@
             writer.println(prefix + '\t' + appWidgets.get(i).toString());
         }
         writer.println(prefix + " ---- folder items ");
-        for (int i = 0; i< folders.size(); i++) {
+        for (int i = 0; i < folders.size(); i++) {
             writer.println(prefix + '\t' + folders.valueAt(i).toString());
         }
+        writer.println(prefix + " ---- extra items ");
+        for (int i = 0; i < extraItems.size(); i++) {
+            writer.println(prefix + '\t' + extraItems.valueAt(i).toString());
+        }
         writer.println(prefix + " ---- items id map ");
-        for (int i = 0; i< itemsIdMap.size(); i++) {
+        for (int i = 0; i < itemsIdMap.size(); i++) {
             writer.println(prefix + '\t' + itemsIdMap.valueAt(i).toString());
         }
 
@@ -442,6 +446,20 @@
             this.containerId = containerId;
             this.items = Collections.unmodifiableList(items);
         }
+
+        @Override
+        @NonNull
+        public final String toString() {
+            StringBuilder s = new StringBuilder();
+            s.append("FixedContainerItems:");
+            s.append(" id=").append(containerId);
+            s.append(" itemCount=").append(items.size());
+            for (int i = 0; i < items.size(); i++) {
+                s.append(" item #").append(i).append(": ").append(items.get(i).toString());
+            }
+            return s.toString();
+        }
+
     }
 
 
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index a7c94bb..26ab5b4 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -20,6 +20,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
+import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
 import static com.android.launcher3.Utilities.dpiFromPx;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_PINNING;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TRANSIENT_TASKBAR;
@@ -32,6 +33,7 @@
 import android.content.ComponentCallbacks;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -82,9 +84,11 @@
     public static final int CHANGE_DENSITY = 1 << 2;
     public static final int CHANGE_SUPPORTED_BOUNDS = 1 << 3;
     public static final int CHANGE_NAVIGATION_MODE = 1 << 4;
+    public static final int CHANGE_TASKBAR_PINNING = 1 << 5;
 
     public static final int CHANGE_ALL = CHANGE_ACTIVE_SCREEN | CHANGE_ROTATION
-            | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE;
+            | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS | CHANGE_NAVIGATION_MODE
+            | CHANGE_TASKBAR_PINNING;
 
     private static final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
     private static final String TARGET_OVERLAY_PACKAGE = "android";
@@ -104,13 +108,17 @@
     private Info mInfo;
     private boolean mDestroyed = false;
 
-    private final LauncherPrefs mPrefs;
+    private SharedPreferences.OnSharedPreferenceChangeListener
+            mTaskbarPinningPreferenceChangeListener;
 
     @VisibleForTesting
     protected DisplayController(Context context) {
         mContext = context;
         mDM = context.getSystemService(DisplayManager.class);
-        mPrefs = LauncherPrefs.get(context);
+
+        if (ENABLE_TASKBAR_PINNING.get()) {
+            attachTaskbarPinningSharedPreferenceChangeListener(mContext);
+        }
 
         Display display = mDM.getDisplay(DEFAULT_DISPLAY);
         if (Utilities.ATLEAST_S) {
@@ -131,6 +139,21 @@
         FileLog.i(TAG, "(CTOR) perDisplayBounds: " + mInfo.mPerDisplayBounds);
     }
 
+    private void attachTaskbarPinningSharedPreferenceChangeListener(Context context) {
+        mTaskbarPinningPreferenceChangeListener =
+                (sharedPreferences, key) -> {
+                    if (TASKBAR_PINNING_KEY.equals(key)
+                            && mInfo.mIsTaskbarPinned != LauncherPrefs.get(mContext).get(
+                            TASKBAR_PINNING)
+                    ) {
+                        handleInfoChange(mWindowContext.getDisplay());
+                    }
+                };
+
+        LauncherPrefs.get(context).addListener(
+                mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
+    }
+
     /**
      * Returns the current navigation mode
      */
@@ -142,25 +165,7 @@
      * Returns whether taskbar is transient.
      */
     public static boolean isTransientTaskbar(Context context) {
-        return INSTANCE.get(context).isTransientTaskbar();
-    }
-
-    /**
-     * Returns whether taskbar is transient.
-     */
-    public boolean isTransientTaskbar() {
-        // TODO(b/258604917): When running in test harness, use !sTransientTaskbarStatusForTests
-        //  once tests are updated to expect new persistent behavior such as not allowing long press
-        //  to stash.
-        if (!Utilities.isRunningInTestHarness()
-                && ENABLE_TASKBAR_PINNING.get()
-                && mPrefs.get(TASKBAR_PINNING)) {
-            return false;
-        }
-        return getInfo().navigationMode == NavigationMode.NO_BUTTON
-                && (Utilities.isRunningInTestHarness()
-                    ? sTransientTaskbarStatusForTests
-                    : ENABLE_TRANSIENT_TASKBAR.get());
+        return INSTANCE.get(context).getInfo().isTransientTaskbar();
     }
 
     /**
@@ -174,6 +179,10 @@
     @Override
     public void close() {
         mDestroyed = true;
+        if (ENABLE_TASKBAR_PINNING.get()) {
+            LauncherPrefs.get(mContext).removeListener(
+                    mTaskbarPinningPreferenceChangeListener, TASKBAR_PINNING);
+        }
         if (mWindowContext != null) {
             mWindowContext.unregisterComponentCallbacks(this);
         } else {
@@ -256,7 +265,8 @@
     }
 
     @AnyThread
-    private void handleInfoChange(Display display) {
+    @VisibleForTesting
+    public void handleInfoChange(Display display) {
         WindowManagerProxy wmProxy = WindowManagerProxy.INSTANCE.get(mContext);
         Info oldInfo = mInfo;
 
@@ -289,6 +299,9 @@
             FileLog.w(TAG,
                     "(CHANGE_SUPPORTED_BOUNDS) perDisplayBounds: " + newInfo.mPerDisplayBounds);
         }
+        if (newInfo.mIsTaskbarPinned != oldInfo.mIsTaskbarPinned) {
+            change |= CHANGE_TASKBAR_PINNING;
+        }
         if (DEBUG) {
             Log.d(TAG, "handleInfoChange - change: " + getChangeFlagsString(change));
         }
@@ -331,6 +344,8 @@
         private final ArrayMap<CachedDisplayInfo, List<WindowBounds>> mPerDisplayBounds =
                 new ArrayMap<>();
 
+        private final boolean mIsTaskbarPinned;
+
         public Info(Context displayInfoContext) {
             /* don't need system overrides for external displays */
             this(displayInfoContext, new WindowManagerProxy(), new ArrayMap<>());
@@ -387,6 +402,26 @@
                 Log.d(TAG, "normalizedDisplayInfo: " + normalizedDisplayInfo);
                 Log.d(TAG, "perDisplayBounds: " + mPerDisplayBounds);
             }
+
+            mIsTaskbarPinned = LauncherPrefs.get(displayInfoContext).get(TASKBAR_PINNING);
+        }
+
+        /**
+         * Returns whether taskbar is transient.
+         */
+        public boolean isTransientTaskbar() {
+            // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
+            //  sTransientTaskbarStatusForTests and update test to directly
+            //  toggle shred preference to switch transient taskbar on/of
+            if (!Utilities.isRunningInTestHarness()
+                    && ENABLE_TASKBAR_PINNING.get()
+                    && mIsTaskbarPinned) {
+                return false;
+            }
+            return navigationMode == NavigationMode.NO_BUTTON
+                    && (Utilities.isRunningInTestHarness()
+                    ? sTransientTaskbarStatusForTests
+                    : ENABLE_TRANSIENT_TASKBAR.get() && !mIsTaskbarPinned);
         }
 
         /**
@@ -426,6 +461,7 @@
         appendFlag(result, change, CHANGE_DENSITY, "CHANGE_DENSITY");
         appendFlag(result, change, CHANGE_SUPPORTED_BOUNDS, "CHANGE_SUPPORTED_BOUNDS");
         appendFlag(result, change, CHANGE_NAVIGATION_MODE, "CHANGE_NAVIGATION_MODE");
+        appendFlag(result, change, CHANGE_TASKBAR_PINNING, "CHANGE_TASKBAR_VARIANT");
         return result.toString();
     }
 
@@ -440,6 +476,7 @@
         pw.println("  fontScale=" + info.fontScale);
         pw.println("  densityDpi=" + info.densityDpi);
         pw.println("  navigationMode=" + info.navigationMode.name());
+        pw.println("  isTaskbarPinned=" + info.mIsTaskbarPinned);
         pw.println("  currentSize=" + info.currentSize);
         info.mPerDisplayBounds.forEach((key, value) -> pw.println(
                 "  perDisplayBounds - " + key + ": " + value));
diff --git a/src/com/android/launcher3/util/Executors.java b/src/com/android/launcher3/util/Executors.java
index dec4b5c..07000ed 100644
--- a/src/com/android/launcher3/util/Executors.java
+++ b/src/com/android/launcher3/util/Executors.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.util;
 
+import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
+
 import android.os.HandlerThread;
 import android.os.Looper;
 import android.os.Process;
@@ -62,7 +64,9 @@
 
     /** A background executor to preinflate views. */
     public static final ExecutorService VIEW_PREINFLATION_EXECUTOR =
-            java.util.concurrent.Executors.newSingleThreadExecutor();
+            java.util.concurrent.Executors.newSingleThreadExecutor(
+                    new SimpleThreadFactory(
+                            "preinflate-allapps-icons", THREAD_PRIORITY_BACKGROUND));
 
     /**
      * Utility method to get a started handler thread statically
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/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index a4b605c..4105a9a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -60,6 +60,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.UserManagerState;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.UserCache;
@@ -681,7 +682,8 @@
         boolean isTwoPane = LARGE_SCREEN_WIDGET_PICKER.get()
                 && launcher.getDeviceProfile().isTablet
                 && launcher.getDeviceProfile().isLandscape
-                && !launcher.getDeviceProfile().isTwoPanels;
+                && (!launcher.getDeviceProfile().isTwoPanels
+                    || FeatureFlags.UNFOLDED_WIDGET_PICKER.get());
 
         WidgetsFullSheet sheet;
         if (isTwoPane) {
diff --git a/tests/Android.bp b/tests/Android.bp
index 1471c08..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",
@@ -57,7 +58,6 @@
       "src/com/android/launcher3/util/rule/SamplerRule.java",
       "src/com/android/launcher3/util/rule/ScreenRecordRule.java",
       "src/com/android/launcher3/util/rule/ShellCommandRule.java",
-      "src/com/android/launcher3/util/rule/TestIsolationRule.java",
       "src/com/android/launcher3/util/rule/TestStabilityRule.java",
       "src/com/android/launcher3/util/rule/TISBindRule.java",
       "src/com/android/launcher3/util/viewcapture_analysis/*.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/AbstractDeviceProfileTest.kt b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index ed8e324..a52ba9e 100644
--- a/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -40,6 +40,7 @@
 import org.junit.Rule
 import org.mockito.ArgumentMatchers
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.`when` as whenever
 
 /**
@@ -306,9 +307,9 @@
             }
         context = runningContext.createConfigurationContext(config)
 
-        val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache)
+        val info = spy(DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache))
         whenever(displayController.info).thenReturn(info)
-        whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode)
+        whenever(info.isTransientTaskbar).thenReturn(isGestureMode)
     }
 
     /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */
diff --git a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index c22cf40..42338bf 100644
--- a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -47,6 +47,7 @@
     protected var transposeLayoutWithOrientation: Boolean = false
     protected var useTwoPanels: Boolean = false
     protected var isGestureMode: Boolean = true
+    protected var isTransientTaskbar: Boolean = true
 
     @Before
     fun setUp() {
@@ -68,7 +69,8 @@
             useTwoPanels,
             isGestureMode,
             DEFAULT_PROVIDER,
-            DEFAULT_DIMENSION_PROVIDER
+            DEFAULT_DIMENSION_PROVIDER,
+            isTransientTaskbar,
         )
 
     protected fun initializeVarsForPhone(
@@ -93,6 +95,7 @@
         whenever(info.smallestSizeDp(any())).thenReturn(411f)
 
         this.isGestureMode = isGestureMode
+        this.isTransientTaskbar = false
         transposeLayoutWithOrientation = true
 
         inv =
@@ -175,6 +178,7 @@
         whenever(info.smallestSizeDp(any())).thenReturn(800f)
 
         this.isGestureMode = isGestureMode
+        this.isTransientTaskbar = true
         useTwoPanels = false
 
         inv =
@@ -258,6 +262,7 @@
         whenever(info.smallestSizeDp(any())).thenReturn(700f)
 
         this.isGestureMode = isGestureMode
+        this.isTransientTaskbar = true
         useTwoPanels = true
 
         inv =
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/celllayout/FavoriteItemsTransaction.java b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
index bf7a21c..fb364ad 100644
--- a/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
+++ b/tests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java
@@ -29,13 +29,13 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.util.ModelTestExtensions;
 
 import java.util.ArrayList;
 import java.util.function.Supplier;
@@ -60,8 +60,7 @@
     public void commit() {
         LauncherModel model = LauncherAppState.getInstance(mContext).getModel();
         // Load the model once so that there is no pending migration:
-        loadModelSync(model);
-
+        ModelTestExtensions.INSTANCE.loadModelSync(model);
         runOnExecutorSync(MODEL_EXECUTOR, () -> {
             ModelDbController controller = model.getModelDbController();
             // Migrate any previous data so that the DB state is correct
@@ -105,16 +104,7 @@
 
         // Reload model
         runOnExecutorSync(MAIN_EXECUTOR, model::forceReload);
-        loadModelSync(model);
-    }
-
-    private void loadModelSync(LauncherModel model) {
-        Callbacks mockCb = new Callbacks() { };
-        runOnExecutorSync(MAIN_EXECUTOR, () -> model.addCallbacksAndLoad(mockCb));
-        runOnExecutorSync(MODEL_EXECUTOR, () -> { });
-
-        runOnExecutorSync(MAIN_EXECUTOR, () -> { });
-        runOnExecutorSync(MAIN_EXECUTOR, () -> model.removeCallbacks(mockCb));
+        ModelTestExtensions.INSTANCE.loadModelSync(model);
     }
 
     /**
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index 00d7ce6..3d388d60 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.celllayout;
 
+import static android.platform.uiautomator_helpers.DeviceHelpers.getContext;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -28,13 +30,16 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.MultipageCellLayout;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.WidgetResizeFrame;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.ModelTestExtensions;
 import com.android.launcher3.util.rule.ShellCommandRule;
 
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
@@ -68,6 +73,13 @@
         TaplTestsLauncher3.initialize(this);
     }
 
+    @After
+    public void tearDown() {
+        ModelTestExtensions.INSTANCE.clearModelDb(
+                LauncherAppState.getInstance(getContext()).getModel()
+        );
+    }
+
     /**
      * Validate if the given board represent the current CellLayout
      **/
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index f734fe5..3e5d717 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -16,9 +16,11 @@
 package com.android.launcher3.ui;
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
+
 import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
 import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -66,7 +68,6 @@
 import com.android.launcher3.util.rule.SamplerRule;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 import com.android.launcher3.util.rule.ShellCommandRule;
-import com.android.launcher3.util.rule.TestIsolationRule;
 import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.launcher3.util.rule.ViewCaptureRule;
 
@@ -209,8 +210,7 @@
         final RuleChain inner = RuleChain
                 .outerRule(new PortraitLandscapeRunner(this))
                 .around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
-                .around(viewCaptureRule)
-                .around(new TestIsolationRule(mLauncher));
+                .around(viewCaptureRule);
 
         return TestHelpers.isInLauncherProcess()
                 ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
diff --git a/tests/src/com/android/launcher3/ui/ActivityAllAppsContainerViewTest.java b/tests/src/com/android/launcher3/ui/ActivityAllAppsContainerViewTest.java
new file mode 100644
index 0000000..ba416ae
--- /dev/null
+++ b/tests/src/com/android/launcher3/ui/ActivityAllAppsContainerViewTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ui;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static com.android.launcher3.model.data.AppInfo.EMPTY_ARRAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.allapps.ActivityAllAppsContainerView;
+import com.android.launcher3.allapps.WorkProfileManager;
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.util.ActivityContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityAllAppsContainerViewTest {
+
+    private static final UserHandle WORK_HANDLE = new UserHandle(13);
+    @Mock
+    private StatsLogManager mStatsLogManager;
+    private AppInfo[] mWorkAppInfo;
+    private ActivityAllAppsContainerView<?> mActivityAllAppsContainerView;
+    private WorkProfileManager mWorkManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = new ActivityContextWrapper(getApplicationContext());
+        mActivityAllAppsContainerView = new ActivityAllAppsContainerView(context);
+        mWorkManager = new WorkProfileManager(context.getSystemService(UserManager.class),
+                mActivityAllAppsContainerView, mStatsLogManager);
+        mActivityAllAppsContainerView.setWorkManager(mWorkManager);
+        ComponentName componentName = new ComponentName(context,
+                "com.android.launcher3.tests.Activity" + "Gmail");
+        AppInfo gmailWorkAppInfo = new AppInfo(componentName, "Gmail", WORK_HANDLE, new Intent());
+        mWorkAppInfo = new AppInfo[]{gmailWorkAppInfo};
+    }
+
+    @Test
+    public void testOnAppsUpdatedWithoutWorkApps_shouldShowTabsIsFalse() {
+        mActivityAllAppsContainerView.getAppsStore().setApps(EMPTY_ARRAY, 0, null);
+
+        mActivityAllAppsContainerView.onAppsUpdated();
+
+        assertThat(mActivityAllAppsContainerView.shouldShowTabs()).isEqualTo(false);
+    }
+
+    @Test
+    public void testOnAppsUpdatedWithWorkApps_shouldShowTabsIsTrue() {
+        mActivityAllAppsContainerView.getAppsStore().setApps(mWorkAppInfo, 0, null);
+
+        mActivityAllAppsContainerView.onAppsUpdated();
+
+        assertThat(mActivityAllAppsContainerView.shouldShowTabs()).isEqualTo(true);
+    }
+}
diff --git a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index bba8c89..f0c4fdb 100644
--- a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -20,13 +20,13 @@
 
 import static com.android.launcher3.BubbleTextView.DISPLAY_ALL_APPS;
 import static com.android.launcher3.BubbleTextView.DISPLAY_PREDICTION_ROW;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TWOLINE_ALLAPPS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
 import android.content.Context;
 import android.graphics.Typeface;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.view.ViewGroup;
 
 import com.android.launcher3.BubbleTextView;
@@ -36,14 +36,11 @@
 import com.android.launcher3.search.StringMatcherUtility;
 import com.android.launcher3.util.ActivityContextWrapper;
 import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.StaticMockitoRule;
 import com.android.launcher3.views.BaseDragLayer;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 /**
@@ -55,7 +52,7 @@
  */
 public class BubbleTextViewTest {
 
-    @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule(Flags.class);
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
     private static final StringMatcherUtility.StringMatcher
             MATCHER = StringMatcherUtility.StringMatcher.getInstance();
     private static final int ONE_LINE = 1;
@@ -75,6 +72,10 @@
             "LEGO®\nBuilder";
     private static final String EMPTY_STRING = "";
     private static final int CHAR_CNT = 7;
+    private static final int MAX_HEIGHT = Integer.MAX_VALUE;
+    private static final int LIMITED_HEIGHT = 357; /* allowedHeight in Pixel6 */
+    private static final float SPACE_MULTIPLIER = 1;
+    private static final float SPACE_EXTRA = 0;
 
     private BubbleTextView mBubbleTextView;
     private ItemInfoWithIcon mItemInfoWithIcon;
@@ -84,7 +85,8 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        Mockito.when(Flags.enableTwolineAllapps()).thenReturn(false);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_CURSOR_HOVER_STATES);
         Utilities.enableRunningInTestHarnessForTests();
         mContext = new ActivityContextWrapper(getApplicationContext());
         mBubbleTextView = new BubbleTextView(mContext);
@@ -111,160 +113,150 @@
 
     @Test
     public void testEmptyString_flagOn() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            mItemInfoWithIcon.title = EMPTY_STRING;
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertNotEquals(TWO_LINE, mBubbleTextView.getMaxLines());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        mItemInfoWithIcon.title = EMPTY_STRING;
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertNotEquals(TWO_LINE, mBubbleTextView.getMaxLines());
     }
 
     @Test
     public void testEmptyString_flagOff() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
-            mItemInfoWithIcon.title = EMPTY_STRING;
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        mItemInfoWithIcon.title = EMPTY_STRING;
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testStringWithSpaceLongerThanCharLimit_flagOn() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            // test string: "Battery Stats"
-            mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "Battery Stats"
+        mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testStringWithSpaceLongerThanCharLimit_flagOff() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
-            // test string: "Battery Stats"
-            mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "Battery Stats"
+        mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringNoSpaceLongerThanCharLimit_flagOn() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            // test string: "flutterappflorafy"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "flutterappflorafy"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringNoSpaceLongerThanCharLimit_flagOff() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
-            // test string: "flutterappflorafy"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw  new RuntimeException(e);
-        }
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "flutterappflorafy"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringWithSpaceLongerThanCharLimit_flagOn() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            // test string: "System UWB Field Test"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw  new RuntimeException(e);
-        }
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "System UWB Field Test"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringWithSpaceLongerThanCharLimit_flagOff() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
-            // test string: "System UWB Field Test"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw  new RuntimeException(e);
-        }
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "System UWB Field Test"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringSymbolLongerThanCharLimit_flagOn() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            // test string: "LEGO®Builder"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw  new RuntimeException(e);
-        }
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "LEGO®Builder"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
     public void testLongStringSymbolLongerThanCharLimit_flagOff() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, false)) {
-            // test string: "LEGO®Builder"
-            mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw  new RuntimeException(e);
-        }
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "LEGO®Builder"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
     }
 
     @Test
@@ -273,8 +265,11 @@
         IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints(
                 TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER);
         CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth,
+                MAX_HEIGHT,
                 TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(),
-                breakPoints);
+                breakPoints,
+                SPACE_MULTIPLIER,
+                SPACE_EXTRA);
         assertEquals(TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString);
     }
 
@@ -283,9 +278,11 @@
         // test string: "flutterappflorafy"
         IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints(
                 TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER);
-        CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth,
+        CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth, 0,
                 TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(),
-                breakPoints);
+                breakPoints,
+                SPACE_MULTIPLIER,
+                SPACE_EXTRA);
         assertEquals(TEST_LONG_STRING_NO_SPACE_LONGER_THAN_CHAR_LIMIT, newString);
     }
 
@@ -295,8 +292,11 @@
         IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints(
                 TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, MATCHER);
         CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth,
+                MAX_HEIGHT,
                 TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(),
-                breakPoints);
+                breakPoints,
+                SPACE_MULTIPLIER,
+                SPACE_EXTRA);
         assertEquals(TEST_LONG_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT_RESULT, newString);
     }
 
@@ -305,25 +305,57 @@
         // test string: "LEGO®Builder"
         IntArray breakPoints = StringMatcherUtility.getListOfBreakpoints(
                 TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, MATCHER);
-        CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(mLimitedWidth,
+        CharSequence newString = BubbleTextView.modifyTitleToSupportMultiLine(
+                mLimitedWidth,
+                MAX_HEIGHT,
                 TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT, mBubbleTextView.getPaint(),
-                breakPoints);
+                breakPoints,
+                SPACE_MULTIPLIER,
+                SPACE_EXTRA);
         assertEquals(TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT_RESULT, newString);
     }
 
     @Test
-    public void testEnsurePredictionRowIsOneLine() {
-        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_TWOLINE_ALLAPPS, true)) {
-            // test string: "Battery Stats"
-            mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
-            mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
-            mBubbleTextView.applyLabel(mItemInfoWithIcon);
-            mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-            mBubbleTextView.measure(mLimitedWidth, 0);
-            mBubbleTextView.onPreDraw();
-            assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
+    public void testEnsurePredictionRowIsTwoLine() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "Battery Stats"
+        mItemInfoWithIcon.title = TEST_STRING_WITH_SPACE_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
+    }
+
+    @Test
+    public void modifyTitleToSupportMultiLine_whenLimitedHeight_shouldBeOneLine() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "LEGO®Builder"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(ONE_LINE, mBubbleTextView.getLineCount());
+    }
+
+    @Test
+    public void modifyTitleToSupportMultiLine_whenUnlimitedHeight_shouldBeTwoLine() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_TWOLINE_ALLAPPS);
+        // test string: "LEGO®Builder"
+        mItemInfoWithIcon.title = TEST_LONG_STRING_SYMBOL_LONGER_THAN_CHAR_LIMIT;
+        mBubbleTextView.setDisplay(DISPLAY_ALL_APPS);
+        mBubbleTextView.applyLabel(mItemInfoWithIcon);
+        mBubbleTextView.setTypeface(Typeface.MONOSPACE);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
+
+        mBubbleTextView.onPreDraw();
+
+        assertEquals(TWO_LINE, mBubbleTextView.getLineCount());
     }
 }
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 8f2ac98..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;
@@ -247,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/src/com/android/launcher3/util/DisplayControllerTest.kt b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
index 8e4e998..a94dd2e 100644
--- a/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
+++ b/tests/src/com/android/launcher3/util/DisplayControllerTest.kt
@@ -30,8 +30,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
 import com.android.launcher3.util.DisplayController.CHANGE_DENSITY
 import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
+import com.android.launcher3.util.DisplayController.CHANGE_TASKBAR_PINNING
 import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.window.CachedDisplayInfo
@@ -89,6 +91,7 @@
         MockitoAnnotations.initMocks(this)
         whenever(context.getObject(eq(WindowManagerProxy.INSTANCE))).thenReturn(windowManagerProxy)
         whenever(context.getObject(eq(LauncherPrefs.INSTANCE))).thenReturn(launcherPrefs)
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
 
         // Mock WindowManagerProxy
         val displayInfo =
@@ -107,6 +110,7 @@
             bounds[i.getArgument<CachedDisplayInfo>(1).rotation]
         }
 
+        whenever(windowManagerProxy.getNavigationMode(any())).thenReturn(NavigationMode.NO_BUTTON)
         // Mock context
         whenever(context.createWindowContext(any(), any(), nullable())).thenReturn(context)
         whenever(context.getSystemService(eq(DisplayManager::class.java)))
@@ -156,4 +160,13 @@
 
         verify(displayInfoChangeListener).onDisplayInfoChanged(any(), any(), eq(CHANGE_DENSITY))
     }
+
+    @Test
+    @UiThreadTest
+    fun testTaskbarPinning() {
+        whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
+        displayController.handleInfoChange(display)
+        verify(displayInfoChangeListener)
+            .onDisplayInfoChanged(any(), any(), eq(CHANGE_TASKBAR_PINNING))
+    }
 }
diff --git a/tests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/src/com/android/launcher3/util/ModelTestExtensions.kt
new file mode 100644
index 0000000..61ec669
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -0,0 +1,30 @@
+package com.android.launcher3.util
+
+import com.android.launcher3.LauncherModel
+import com.android.launcher3.model.BgDataModel
+
+object ModelTestExtensions {
+    /** Clears and reloads Launcher db to cleanup the workspace */
+    fun LauncherModel.clearModelDb() {
+        // Load the model once so that there is no pending migration:
+        loadModelSync()
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {
+            modelDbController.run {
+                tryMigrateDB()
+                createEmptyDB()
+                clearEmptyDbFlag()
+            }
+        }
+        // Reload model
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { forceReload() }
+        loadModelSync()
+    }
+
+    fun LauncherModel.loadModelSync() {
+        val mockCb: BgDataModel.Callbacks = object : BgDataModel.Callbacks {}
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { addCallbacksAndLoad(mockCb) }
+        TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) {}
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
+        TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) { removeCallbacks(mockCb) }
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java b/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java
deleted file mode 100644
index 592cc9b..0000000
--- a/tests/src/com/android/launcher3/util/rule/TestIsolationRule.java
+++ /dev/null
@@ -1,50 +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.launcher3.util.rule;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.tapl.LauncherInstrumentation;
-import com.android.launcher3.ui.AbstractLauncherUiTest;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-/**
- * Isolates tests from some of the state created by the previous test.
- */
-public class TestIsolationRule implements TestRule {
-    final LauncherInstrumentation mLauncher;
-
-    public TestIsolationRule(LauncherInstrumentation launcher) {
-        mLauncher = launcher;
-    }
-
-    @NonNull
-    @Override
-    public Statement apply(@NonNull Statement base, @NonNull Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                base.evaluate();
-                // Make sure that Launcher workspace looks correct.
-                mLauncher.goHome();
-                AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
-            }
-        };
-    }
-}
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 71ca77f..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,7 +762,7 @@
         return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
     }
 
-    private UiObject2 verifyContainerType(ContainerType containerType) {
+    UiObject2 verifyContainerType(ContainerType containerType) {
         waitForLauncherInitialized();
 
         if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
@@ -784,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);
@@ -798,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);
@@ -813,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);
                 }
@@ -821,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);
@@ -841,6 +852,7 @@
                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     }
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
@@ -855,6 +867,7 @@
                     }
 
                     waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case LAUNCHED_APP: {
@@ -863,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;
@@ -981,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.
@@ -1170,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));
     }
@@ -1735,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();
@@ -1772,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 {
 
     /**