Merge "Moving test related to the app icon menu to their own file" into main
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/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/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/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/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/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 3429df1..419824a 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -137,6 +137,7 @@
     private ISplitSelectListener mSplitSelectListener;
     private IStartingWindowListener mStartingWindowListener;
     private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
+    private String mLauncherActivityClass;
     private IRecentTasksListener mRecentTasksListener;
     private IUnfoldTransitionListener mUnfoldAnimationListener;
     private IDesktopTaskListener mDesktopTaskListener;
@@ -248,7 +249,8 @@
         registerSplitScreenListener(mSplitScreenListener);
         registerSplitSelectListener(mSplitSelectListener);
         setStartingWindowListener(mStartingWindowListener);
-        setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
+        setLauncherUnlockAnimationController(
+                mLauncherActivityClass, mLauncherUnlockAnimationController);
         new LinkedHashMap<>(mRemoteTransitions).forEach(this::registerRemoteTransition);
         setupTransactionQueue();
         registerRecentTasksListener(mRecentTasksListener);
@@ -1109,11 +1111,11 @@
      * changes).
      */
     public void setLauncherUnlockAnimationController(
-            ILauncherUnlockAnimationController controller) {
+            String activityClass, ILauncherUnlockAnimationController controller) {
         if (mSysuiUnlockAnimationController != null) {
             try {
-                mSysuiUnlockAnimationController.setLauncherUnlockController(controller);
-
+                mSysuiUnlockAnimationController.setLauncherUnlockController(
+                        activityClass, controller);
                 if (controller != null) {
                     controller.dispatchSmartspaceStateToSysui();
                 }
@@ -1121,7 +1123,7 @@
                 Log.w(TAG, "Failed call setLauncherUnlockAnimationController", e);
             }
         }
-
+        mLauncherActivityClass = activityClass;
         mLauncherUnlockAnimationController = controller;
     }
 
diff --git a/quickstep/src/com/android/quickstep/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/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/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 8888c0d..825c0ae 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2277,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();
@@ -2361,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++) {
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/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/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index bf35a0f..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;
@@ -2123,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) {
@@ -2177,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");
@@ -2198,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 8707aba..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);
                     }
                 });
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/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/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index ae0d9f0..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 {
@@ -196,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,
@@ -438,13 +436,19 @@
     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/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/tests/Android.bp b/tests/Android.bp
index d828fdf..654edad 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -58,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/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/BubbleTextViewTest.java b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
index e6fdbaa..f0c4fdb 100644
--- a/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
+++ b/tests/src/com/android/launcher3/ui/BubbleTextViewTest.java
@@ -323,7 +323,7 @@
         mBubbleTextView.setDisplay(DISPLAY_PREDICTION_ROW);
         mBubbleTextView.applyLabel(mItemInfoWithIcon);
         mBubbleTextView.setTypeface(Typeface.MONOSPACE);
-        mBubbleTextView.measure(mLimitedWidth, LIMITED_HEIGHT);
+        mBubbleTextView.measure(mLimitedWidth, MAX_HEIGHT);
 
         mBubbleTextView.onPreDraw();
 
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 07eeb2a..bc53d6d 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -86,7 +86,6 @@
     public static void initialize(
             AbstractLauncherUiTest test, boolean clearWorkspace) throws Exception {
         test.reinitializeLauncherData(clearWorkspace);
-        test.mLauncher.resetFreezeRecentTaskList();
         test.mDevice.pressHome();
         test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
         test.waitForState("Launcher internal state didn't switch to Home",
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/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 b7d2530..202c078 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -20,7 +20,10 @@
 import static android.content.pm.PackageManager.DONT_KILL_APP;
 import static android.content.pm.PackageManager.MATCH_ALL;
 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
 import static android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT;
+
 import static com.android.launcher3.tapl.Folder.FOLDER_CONTENT_RES_ID;
 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -49,6 +52,9 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
@@ -170,6 +176,7 @@
     private static final String OPEN_FOLDER_RES_ID = "folder_content";
     static final String TASKBAR_RES_ID = "taskbar_view";
     private static final String SPLIT_PLACEHOLDER_RES_ID = "split_placeholder";
+    static final String KEYBOARD_QUICK_SWITCH_RES_ID = "keyboard_quick_switch_view";
     public static final int WAIT_TIME_MS = 30000;
     static final long DEFAULT_POLL_INTERVAL = 1000;
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
@@ -755,18 +762,7 @@
         return isTablet() ? getLauncherPackageName() : SYSTEMUI_PACKAGE;
     }
 
-    /**
-     * Resets the frozen recent tasks list if necessary from a previous quickswitch.
-     */
-    public void resetFreezeRecentTaskList() {
-        try {
-            mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
-        } catch (IOException e) {
-            Log.e(TAG, "Failed to reset fozen recent tasks list", e);
-        }
-    }
-
-    private UiObject2 verifyContainerType(ContainerType containerType) {
+    UiObject2 verifyContainerType(ContainerType containerType) {
         waitForLauncherInitialized();
 
         if (mExpectedRotationCheckEnabled && mExpectedRotation != null) {
@@ -795,6 +791,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -809,6 +806,7 @@
                     waitUntilLauncherObjectGone(APPS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -824,6 +822,7 @@
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForLauncherObject(APPS_RES_ID);
                 }
@@ -832,6 +831,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (is3PLauncher() && isTablet()) {
                         waitForSystemLauncherObject(TASKBAR_RES_ID);
@@ -852,6 +852,7 @@
                         waitUntilSystemLauncherObjectGone(TASKBAR_RES_ID);
                     }
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
@@ -866,6 +867,7 @@
                     }
 
                     waitForSystemLauncherObject(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
                     return waitForSystemLauncherObject(OVERVIEW_RES_ID);
                 }
                 case LAUNCHED_APP: {
@@ -874,6 +876,7 @@
                     waitUntilLauncherObjectGone(WIDGETS_RES_ID);
                     waitUntilSystemLauncherObjectGone(OVERVIEW_RES_ID);
                     waitUntilSystemLauncherObjectGone(SPLIT_PLACEHOLDER_RES_ID);
+                    waitUntilLauncherObjectGone(KEYBOARD_QUICK_SWITCH_RES_ID);
 
                     if (mIgnoreTaskbarVisibility) {
                         return null;
@@ -1181,6 +1184,14 @@
         }
     }
 
+    LaunchedAppState assertAppLaunched(@NonNull String expectedPackageName) {
+        BySelector packageSelector = By.pkg(expectedPackageName);
+        assertTrue("App didn't start: (" + packageSelector + ")",
+                mDevice.wait(Until.hasObject(packageSelector),
+                        LauncherInstrumentation.WAIT_TIME_MS));
+        return new LaunchedAppState(this);
+    }
+
     void waitUntilLauncherObjectGone(String resId) {
         waitUntilGoneBySelector(getLauncherObjectSelector(resId));
     }
@@ -1746,6 +1757,12 @@
                 InputDevice.SOURCE_TOUCHSCREEN);
     }
 
+    private void injectEvent(InputEvent event) {
+        assertTrue("injectInputEvent failed: event=" + event,
+                mInstrumentation.getUiAutomation().injectInputEvent(event, true, false));
+        event.recycle();
+    }
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope, int source) {
         final boolean hasTIS = hasTIS();
@@ -1783,9 +1800,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 {
 
     /**