Merge "Adding a keyboard shortcut delegarte to move logic outside of the launcher" 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/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/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 c454a91..4215e31 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -426,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) {
@@ -671,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));
@@ -788,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) {
@@ -1177,10 +1129,6 @@
         mDeferOverlayCallbacks = true;
     }
 
-    public LauncherOverlayManager getOverlayManager() {
-        return mOverlayManager;
-    }
-
     @Override
     public void onStateSetStart(LauncherState state) {
         super.onStateSetStart(state);
@@ -1588,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()) {
@@ -1894,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) {
@@ -2144,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() {
@@ -2212,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);
@@ -2248,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;
@@ -3109,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
@@ -3237,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
@@ -3312,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);
     }
@@ -3324,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.
@@ -3371,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.
@@ -3418,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};
     }
 
     /**
@@ -3437,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/tests/Android.bp b/tests/Android.bp
index 1471c08..e1b97de 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -57,7 +57,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/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/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);
-            }
-        };
-    }
-}