Merge "Convert Launcher Android.mk to Android.bp to setup RoboTests" into sc-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 56a2595..1fad72d 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/go/AndroidManifest-launcher.xml b/go/AndroidManifest-launcher.xml
index 2223036..d2575b6 100644
--- a/go/AndroidManifest-launcher.xml
+++ b/go/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|uiMode"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 53910e3..7fe9b08 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -49,7 +49,7 @@
             android:stateNotNeeded="true"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="unspecified"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 97f4a21..5e5cf73 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -71,7 +71,7 @@
              android:stateNotNeeded="true"
              android:theme="@style/LauncherTheme"
              android:screenOrientation="unspecified"
-             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+             android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
              android:resizeableActivity="true"
              android:resumeWhilePausing="true"
              android:taskAffinity=""/>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index e46eb9e..9773366 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -29,6 +29,7 @@
     <dimen name="overview_actions_bottom_margin_three_button">8dp</dimen>
     <dimen name="overview_actions_horizontal_margin">16dp</dimen>
 
+    <dimen name="recents_row_spacing">48dp</dimen>
     <dimen name="recents_page_spacing">16dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
 
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 5b30143..161c98e 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -35,6 +35,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.model.WellbeingModel;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.proxy.ProxyActivityStarter;
@@ -83,6 +84,8 @@
 
     private @Nullable TaskbarController mTaskbarController;
     private final TaskbarStateHandler mTaskbarStateHandler = new TaskbarStateHandler(this);
+    // Will be updated when dragging from taskbar.
+    private DragOptions mWorkspaceDragOptions = new DragOptions();
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -270,6 +273,15 @@
     }
 
     @Override
+    public DragOptions getDefaultWorkspaceDragOptions() {
+        return mWorkspaceDragOptions;
+    }
+
+    public void setWorkspaceDragOptions(DragOptions dragOptions) {
+        mWorkspaceDragOptions = dragOptions;
+    }
+
+    @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
         QuickstepAppTransitionManagerImpl appTransitionManager =
                 (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 0156e8f..f297343 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -107,7 +107,8 @@
         WorkspaceItemInfo dragItem = new WorkspaceItemInfo((WorkspaceItemInfo) v.getTag());
         v.setVisibility(View.INVISIBLE);
         mLauncher.getWorkspace().beginDragShared(
-                v, null, this, dragItem, new DragPreviewProvider(v), new DragOptions());
+                v, null, this, dragItem, new DragPreviewProvider(v),
+                mLauncher.getDefaultWorkspaceDragOptions());
         return true;
     };
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 06372fe..8312b82 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -35,13 +35,14 @@
     private final DeviceProfile mDeviceProfile;
     private final LayoutInflater mLayoutInflater;
     private final TaskbarContainerView mTaskbarContainerView;
+    private final float mIconScale;
 
     public TaskbarActivityContext(BaseQuickstepLauncher launcher) {
         super(launcher);
         mDeviceProfile = launcher.getDeviceProfile().copy(this);
         float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
-        float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
-        mDeviceProfile.updateIconSize(iconScale, getResources());
+        mIconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
+        mDeviceProfile.updateIconSize(mIconScale, getResources());
 
         mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
 
@@ -75,4 +76,11 @@
     public Rect getFolderBoundingBox() {
         return mTaskbarContainerView.getFolderBoundingBox();
     }
+
+    /**
+     * @return The ratio of taskbar icon size vs normal workspace/hotseat icon size.
+     */
+    public float getTaskbarIconScale() {
+        return mIconScale;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
index ddd0d15..1e5e3e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarContainerView.java
@@ -97,7 +97,12 @@
             // to show a floating view like Folder. Thus, we set the contentInsets to be where
             // mTaskbarView is, since its position never changes and insets rather than overlays.
             int[] loc = mTempLoc;
+            float scale = mTaskbarView.getScaleX();
+            mTaskbarView.setScaleX(1);
+            mTaskbarView.setScaleY(1);
             mTaskbarView.getLocationInWindow(loc);
+            mTaskbarView.setScaleX(scale);
+            mTaskbarView.setScaleY(scale);
             insetsInfo.contentInsets.left = loc[0];
             insetsInfo.contentInsets.top = loc[1];
             insetsInfo.contentInsets.right = getWidth() - (loc[0] + mTaskbarView.getWidth());
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index 5dddaf3..74a82ab 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -19,14 +19,18 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
 import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.app.ActivityOptions;
 import android.content.ComponentName;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
@@ -81,6 +85,8 @@
     // Contains all loaded Hotseat items.
     private ItemInfo[] mLatestLoadedHotseatItems;
 
+    private boolean mIsAnimatingToLauncher;
+
     public TaskbarController(BaseQuickstepLauncher launcher,
             TaskbarContainerView taskbarContainerView) {
         mLauncher = launcher;
@@ -164,7 +170,22 @@
 
             @Override
             public View.OnLongClickListener getItemOnLongClickListener() {
-                return mDragController::startDragOnLongClick;
+                return view -> {
+                    if (mLauncher.hasBeenResumed() && view.getTag() instanceof ItemInfo) {
+                        alignRealHotseatWithTaskbar();
+                        return mDragController.startWorkspaceDragOnLongClick(view);
+                    } else {
+                        return mDragController.startSystemDragOnLongClick(view);
+                    }
+                };
+            }
+
+            @Override
+            public int getEmptyHotseatViewVisibility() {
+                // When on the home screen, we want the empty hotseat views to take up their full
+                // space so that the others line up with the home screen hotseat.
+                return mLauncher.hasBeenResumed() || mIsAnimatingToLauncher
+                        ? View.INVISIBLE : View.GONE;
             }
         };
     }
@@ -207,6 +228,8 @@
         mTaskbarVisibilityController.init();
         mHotseatController.init();
         mRecentsController.init();
+
+        SCALE_PROPERTY.set(mTaskbarView, mLauncher.hasBeenResumed() ? getTaskbarScaleOnHome() : 1f);
     }
 
     private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
@@ -290,11 +313,38 @@
         if (toState != null) {
             mTaskbarStateHandler.setStateWithAnimation(toState, new StateAnimationConfig(), anim);
         }
+        anim.addFloat(mTaskbarView, SCALE_PROPERTY, mTaskbarView.getScaleX(),
+                getTaskbarScaleOnHome(), LINEAR);
+
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mIsAnimatingToLauncher = true;
+                mTaskbarView.updateHotseatItemsVisibility();
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mIsAnimatingToLauncher = false;
+            }
+        });
+
+        anim.addOnFrameCallback(this::alignRealHotseatWithTaskbar);
+
         return anim.buildAnim();
     }
 
     private Animator createAnimToApp(long duration) {
-        return mTaskbarVisibilityController.createAnimToBackgroundAlpha(1, duration);
+        PendingAnimation anim = new PendingAnimation(duration);
+        anim.add(mTaskbarVisibilityController.createAnimToBackgroundAlpha(1, duration));
+        anim.addFloat(mTaskbarView, SCALE_PROPERTY, mTaskbarView.getScaleX(), 1f, LINEAR);
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mTaskbarView.updateHotseatItemsVisibility();
+            }
+        });
+        return anim.buildAnim();
     }
 
     /**
@@ -378,6 +428,21 @@
     }
 
     /**
+     * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
+     */
+    public void alignRealHotseatWithTaskbar() {
+        Rect hotseatBounds = new Rect();
+        mTaskbarView.getHotseatBoundsAtScale(getTaskbarScaleOnHome()).roundOut(hotseatBounds);
+        mLauncher.getHotseat().setPadding(hotseatBounds.left, hotseatBounds.top,
+                mTaskbarView.getWidth() - hotseatBounds.right,
+                mTaskbarView.getHeight() - hotseatBounds.bottom);
+    }
+
+    private float getTaskbarScaleOnHome() {
+        return 1f / mTaskbarContainerView.getTaskbarActivityContext().getTaskbarIconScale();
+    }
+
+    /**
      * Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
      */
     private void setTaskbarWindowFullscreen(boolean fullscreen) {
@@ -420,6 +485,7 @@
     protected interface TaskbarViewCallbacks {
         View.OnClickListener getItemOnClickListener();
         View.OnLongClickListener getItemOnLongClickListener();
+        int getEmptyHotseatViewVisibility();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index baec899..f51e498 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ClipDescriptionCompat;
@@ -57,7 +58,7 @@
      * generate the ClipDescription and Intent.
      * @return Whether {@link View#startDragAndDrop} started successfully.
      */
-    protected boolean startDragOnLongClick(View view) {
+    protected boolean startSystemDragOnLongClick(View view) {
         if (!(view instanceof BubbleTextView)) {
             return false;
         }
@@ -125,6 +126,38 @@
     }
 
     /**
+     * Starts a drag and drop operation that controls a real Workspace (Hotseat) view.
+     * @param view The Taskbar item that was long clicked.
+     * @return Whether {@link View#startDragAndDrop} started successfully.
+     */
+    protected boolean startWorkspaceDragOnLongClick(View view) {
+        View.DragShadowBuilder transparentShadowBuilder = new View.DragShadowBuilder(view) {
+            private static final int ARBITRARY_SHADOW_SIZE = 10;
+
+            @Override
+            public void onDrawShadow(Canvas canvas) {
+            }
+
+            @Override
+            public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
+                outShadowSize.set(ARBITRARY_SHADOW_SIZE, ARBITRARY_SHADOW_SIZE);
+                outShadowTouchPoint.set(ARBITRARY_SHADOW_SIZE / 2, ARBITRARY_SHADOW_SIZE / 2);
+            }
+        };
+
+        TaskbarDragListener taskbarDragListener = new TaskbarDragListener(mLauncher,
+                (ItemInfo) view.getTag());
+        if (view.startDragAndDrop(new ClipData("", new String[] {taskbarDragListener.getMimeType()},
+                        new ClipData.Item("")),
+                transparentShadowBuilder, null /* localState */, View.DRAG_FLAG_GLOBAL)) {
+            view.setOnDragListener(getDraggedViewDragListener());
+            taskbarDragListener.init(mLauncher.getDragLayer());
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Hide the original Taskbar item while it is being dragged.
      */
     private View.OnDragListener getDraggedViewDragListener() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
new file mode 100644
index 0000000..2bd5861
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragListener.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.content.ClipDescription;
+import android.graphics.Point;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.UUID;
+
+/**
+ * Listens to system drag and drop events initated by the Taskbar, and forwards them to Launcher's
+ * internal DragController to move Hotseat items.
+ */
+public class TaskbarDragListener implements View.OnDragListener {
+
+    private static final String MIME_TYPE_PREFIX = "com.android.launcher3.taskbar.drag_and_drop/";
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final ItemInfo mDraggedItem;
+    private final DragOptions mDragOptions;
+    // Randomly generated id used to verify the drag event.
+    private final String mId;
+
+    // Initialized in init().
+    DragLayer mDragLayer;
+
+    /**
+     * @param draggedItem The info of the item that was long clicked, which we will use to find
+     *                    the equivalent match on Hotseat to drag internally.
+     */
+    public TaskbarDragListener(BaseQuickstepLauncher launcher, ItemInfo draggedItem) {
+        mLauncher = launcher;
+        mDraggedItem = draggedItem;
+        mDragOptions = new DragOptions();
+        mDragOptions.simulatedDndStartPoint = new Point();
+        mId = UUID.randomUUID().toString();
+    }
+
+    protected void init(DragLayer dragLayer) {
+        mDragLayer = dragLayer;
+        mDragLayer.setOnDragListener(this);
+    }
+
+    private void cleanup() {
+        mDragLayer.setOnDragListener(null);
+        mLauncher.setWorkspaceDragOptions(new DragOptions());
+    }
+
+    /**
+     * Returns a randomly generated id used to verify the drag event.
+     */
+    protected String getMimeType() {
+        return MIME_TYPE_PREFIX + mId;
+    }
+
+    @Override
+    public boolean onDrag(View dragLayer, DragEvent dragEvent) {
+        ClipDescription clipDescription = dragEvent.getClipDescription();
+        if (dragEvent.getAction() == DragEvent.ACTION_DRAG_STARTED) {
+            if (clipDescription == null || !clipDescription.hasMimeType(getMimeType())) {
+                // We didn't initiate this drag, ignore.
+                cleanup();
+                return false;
+            }
+            View hotseatView = mLauncher.getHotseat().getFirstItemMatch(
+                    (info, view) -> info == mDraggedItem);
+            if (hotseatView == null) {
+                cleanup();
+                return false;
+            }
+            mDragOptions.simulatedDndStartPoint.set((int) dragEvent.getX(), (int) dragEvent.getY());
+            mLauncher.setWorkspaceDragOptions(mDragOptions);
+            hotseatView.performLongClick();
+        } else if (dragEvent.getAction() == DragEvent.ACTION_DRAG_ENDED) {
+            cleanup();
+        }
+        return mLauncher.getDragController().onDragEvent(dragEvent);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 7a13b89..a729e77 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Matrix;
 import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -35,6 +36,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
@@ -50,10 +52,12 @@
     private final ColorDrawable mBackgroundDrawable;
     private final int mItemMarginLeftRight;
     private final int mIconTouchSize;
+    private final boolean mIsRtl;
     private final int mTouchSlop;
     private final RectF mTempDelegateBounds = new RectF();
     private final RectF mDelegateSlopBounds = new RectF();
     private final int[] mTempOutLocation = new int[2];
+    private final Matrix mTempMatrix = new Matrix();
 
     // Initialized in TaskbarController constructor.
     private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
@@ -94,6 +98,7 @@
         mBackgroundDrawable = (ColorDrawable) getBackground();
         mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
         mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+        mIsRtl = Utilities.isRtl(resources);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
@@ -131,7 +136,8 @@
      */
     protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
         for (int i = 0; i < hotseatItemInfos.length; i++) {
-            ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+            ItemInfo hotseatItemInfo = hotseatItemInfos[!mIsRtl ? i
+                    : hotseatItemInfos.length - i - 1];
             int hotseatIndex = mHotseatStartIndex + i;
             View hotseatView = getChildAt(hotseatIndex);
 
@@ -176,25 +182,45 @@
                     && hotseatItemInfo instanceof WorkspaceItemInfo) {
                 ((BubbleTextView) hotseatView).applyFromWorkspaceItem(
                         (WorkspaceItemInfo) hotseatItemInfo);
-                hotseatView.setVisibility(VISIBLE);
                 hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
                 hotseatView.setOnLongClickListener(
                         mControllerCallbacks.getItemOnLongClickListener());
             } else if (isFolder) {
-                hotseatView.setVisibility(VISIBLE);
                 hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
                 hotseatView.setOnLongClickListener(
                         mControllerCallbacks.getItemOnLongClickListener());
             } else {
-                hotseatView.setVisibility(GONE);
                 hotseatView.setOnClickListener(null);
                 hotseatView.setOnLongClickListener(null);
+                hotseatView.setTag(null);
             }
+            updateHotseatItemVisibility(hotseatView);
         }
 
         updateHotseatRecentsDividerVisibility();
     }
 
+    protected void updateHotseatItemsVisibility() {
+        for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+            updateHotseatItemVisibility(getChildAt(i));
+        }
+    }
+
+    private void updateHotseatItemVisibility(View hotseatView) {
+        if (hotseatView.getTag() != null) {
+            hotseatView.setVisibility(VISIBLE);
+        } else {
+            int oldVisibility = hotseatView.getVisibility();
+            int newVisibility = mControllerCallbacks.getEmptyHotseatViewVisibility();
+            hotseatView.setVisibility(newVisibility);
+            if (oldVisibility == GONE && newVisibility != GONE) {
+                // By default, the layout transition only runs when going to VISIBLE,
+                // but we want it to run when going to GONE to INVISIBLE as well.
+                getLayoutTransition().showChild(this, hotseatView, oldVisibility);
+            }
+        }
+    }
+
     private View addDivider(int dividerIndex) {
         View divider = inflate(R.layout.taskbar_divider);
         addView(divider, dividerIndex);
@@ -390,6 +416,35 @@
         return mIsDraggingItem;
     }
 
+    /**
+     * @return The bounding box of where the hotseat elements will be when we reach the given scale.
+     */
+    protected RectF getHotseatBoundsAtScale(float taskbarViewScale) {
+        View firstHotseatView = null, lastHotseatView = null;
+        for (int i = mHotseatStartIndex; i <= mHotseatEndIndex; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                if (firstHotseatView == null) {
+                    firstHotseatView = child;
+                }
+                lastHotseatView = child;
+            }
+        }
+        if (firstHotseatView == null || lastHotseatView == null) {
+            return new RectF();
+        }
+        View leftmostHotseatView = !mIsRtl ? firstHotseatView : lastHotseatView;
+        View rightmostHotseatView = !mIsRtl ? lastHotseatView : firstHotseatView;
+        RectF hotseatBounds = new RectF(
+                leftmostHotseatView.getLeft() - mItemMarginLeftRight,
+                leftmostHotseatView.getTop(),
+                rightmostHotseatView.getRight() + mItemMarginLeftRight,
+                rightmostHotseatView.getBottom());
+        mTempMatrix.setScale(taskbarViewScale, taskbarViewScale, getPivotX(), getPivotY());
+        mTempMatrix.mapRect(hotseatBounds);
+        return hotseatBounds;
+    }
+
     // FolderIconParent implemented methods.
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index aad7e17..0f13ef9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
@@ -73,6 +74,8 @@
         SCRIM_PROGRESS.set(scrim, state.getOverviewScrimAlpha(mLauncher));
         SCRIM_MULTIPLIER.set(scrim, 1f);
         getTaskModalnessProperty().set(mRecentsView, state.getOverviewModalness());
+        RECENTS_GRID_PROGRESS.set(mRecentsView, state.displayOverviewTasksAsGrid(mLauncher)
+                ? 1f : 0f);
     }
 
     @Override
@@ -117,6 +120,8 @@
                 mRecentsView, getTaskModalnessProperty(),
                 toState.getOverviewModalness(),
                 config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
+        setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
+                toState.displayOverviewTasksAsGrid(mLauncher) ? 1f : 0f, LINEAR);
     }
 
     abstract FloatProperty getTaskModalnessProperty();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
index 010694b..bb1f6fc 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -60,6 +60,14 @@
         mListeners.add(r);
     }
 
+    @Override
+    public void removeChangeListener(Runnable r) {
+        if (mListeners == null) {
+            return;
+        }
+        mListeners.remove(r);
+    }
+
     private void registerDeviceConfigChangedListener(Context context) {
         DeviceConfig.addOnPropertiesChangedListener(
                 NAMESPACE_LAUNCHER,
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 98551fb..d330a68 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -228,17 +228,13 @@
      */
     public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
 
-        private int mOffsetX;
-        private int mOffsetY;
-        private int mIconRadius;
-        private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private final PredictedAppIcon mIcon;
+        private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
 
         public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
             mDelegateCellX = cellX;
             mDelegateCellY = cellY;
-            mOffsetX = icon.getOutlineOffsetX();
-            mOffsetY = icon.getOutlineOffsetY();
-            mIconRadius = icon.mNormalizedIconRadius;
+            mIcon = icon;
             mOutlinePaint.setStyle(Paint.Style.FILL);
             mOutlinePaint.setColor(Color.argb(24, 245, 245, 245));
         }
@@ -248,7 +244,8 @@
          */
         @Override
         public void drawUnderItem(Canvas canvas) {
-            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+            getShape().drawShape(canvas, mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY(),
+                    mIcon.mNormalizedIconRadius, mOutlinePaint);
         }
 
         /**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 5ccc1e8..c9de662 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,7 +15,8 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE;
 import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
@@ -83,11 +84,14 @@
 
     private void setAlphas(PropertySetter propertySetter, StateAnimationConfig config,
             LauncherState state) {
-        float buttonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1 : 0;
+        float clearAllButtonAlpha = (state.getVisibleElements(mLauncher) & CLEAR_ALL_BUTTON) != 0
+                ? 1 : 0;
         propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
-                buttonAlpha, LINEAR);
+                clearAllButtonAlpha, LINEAR);
+        float overviewButtonAlpha = (state.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0
+                ? 1 : 0;
         propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
-                MultiValueAlpha.VALUE, buttonAlpha, config.getInterpolator(
+                MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
                         ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index 2cf65af..2ad718b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -69,12 +69,18 @@
     @Override
     public int getVisibleElements(Launcher launcher) {
         return super.getVisibleElements(launcher)
-                & ~OVERVIEW_BUTTONS
+                & ~OVERVIEW_ACTIONS
+                & ~CLEAR_ALL_BUTTON
                 & ~VERTICAL_SWIPE_INDICATOR
                 | TASKBAR;
     }
 
     @Override
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return false;
+    }
+
+    @Override
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
         if ((getVisibleElements(launcher) & HOTSEAT_ICONS) != 0) {
             // Translate hotseat offscreen if we show it in overview.
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
index 41c689d..bdba482 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewModalTaskState.java
@@ -45,7 +45,7 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return OVERVIEW_BUTTONS;
+        return OVERVIEW_ACTIONS | CLEAR_ALL_BUTTON;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index b295e79..d480b6d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.RecentsView;
@@ -122,7 +123,8 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return OVERVIEW_BUTTONS;
+        return displayOverviewTasksAsGrid(launcher) ? CLEAR_ALL_BUTTON
+                : CLEAR_ALL_BUTTON | OVERVIEW_ACTIONS;
     }
 
     @Override
@@ -131,6 +133,11 @@
     }
 
     @Override
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return launcher.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+    }
+
+    @Override
     public String getDescription(Launcher launcher) {
         return launcher.getString(R.string.accessibility_recent_apps);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
index 69b8aca..473fe2d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/QuickstepAtomicAnimationFactory.java
@@ -18,6 +18,7 @@
 import static android.view.View.VISIBLE;
 
 import static com.android.launcher3.LauncherState.HINT_STATE;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.WorkspaceStateTransitionAnimation.getSpringScaleAnimator;
@@ -32,6 +33,7 @@
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_DEPTH;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
@@ -48,7 +50,6 @@
 
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.Hotseat;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -62,7 +63,7 @@
  * Animation factory for quickstep specific transitions
  */
 public class QuickstepAtomicAnimationFactory extends
-        RecentsAtomicAnimationFactory<Launcher, LauncherState> {
+        RecentsAtomicAnimationFactory<QuickstepLauncher, LauncherState> {
 
     // Scale recents takes before animating in
     private static final float RECENTS_PREPARE_SCALE = 1.33f;
@@ -149,6 +150,17 @@
                 mHintToNormalDuration = (int) va.getDuration();
             }
             config.duration = Math.max(config.duration, mHintToNormalDuration);
+        } else if (mActivity.getTaskbarController() != null)  {
+            boolean wasHotseatVisible = fromState.areElementsVisible(mActivity, HOTSEAT_ICONS);
+            boolean isHotseatVisible = toState.areElementsVisible(mActivity, HOTSEAT_ICONS);
+            if (wasHotseatVisible || isHotseatVisible) {
+                config.setInterpolator(ANIM_TASKBAR_FADE, INSTANT);
+                config.setInterpolator(ANIM_HOTSEAT_FADE, INSTANT);
+
+                if (isHotseatVisible) {
+                    mActivity.getTaskbarController().alignRealHotseatWithTaskbar();
+                }
+            }
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 6b9c340..a990f3e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -24,8 +24,8 @@
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.animation.ValueAnimator;
@@ -145,7 +145,7 @@
                     OverviewScrim.SCRIM_MULTIPLIER, OVERVIEW_TO_HOME_SCRIM_MULTIPLIER,
                     PULLBACK_INTERPOLATOR);
 
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 builder.addOnFrameCallback(recentsView::redrawLiveTile);
             }
 
@@ -194,7 +194,7 @@
         boolean success = interpolatedProgress >= SUCCESS_TRANSITION_PROGRESS
                 || (velocity < 0 && fling);
         if (success) {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mLauncher.getOverviewPanel();
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, null));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index df433f8..4766870 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -18,7 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.newCancelListener;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
+import static com.android.launcher3.LauncherState.OVERVIEW_ACTIONS;
 import static com.android.launcher3.LauncherState.QUICK_SWITCH;
 import static com.android.launcher3.anim.AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
@@ -222,7 +222,7 @@
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
         mLauncher.getActionsView().getVisibilityAlpha().setValue(
-                (fromState.getVisibleElements(mLauncher) & OVERVIEW_BUTTONS) != 0 ? 1f : 0f);
+                (fromState.getVisibleElements(mLauncher) & OVERVIEW_ACTIONS) != 0 ? 1f : 0f);
 
         float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 845699a..4df0f63 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -76,7 +76,7 @@
      * @return the animation
      */
     PendingAnimation createSwipeDownToTaskAppAnimation(long duration, Interpolator interpolator) {
-        mRecentsView.setCurrentPage(mRecentsView.getPageNearestToCenterOfScreen());
+        mRecentsView.setCurrentPage(mRecentsView.getDestinationPage());
         TaskView taskView = mRecentsView.getCurrentPageTaskView();
         if (taskView == null) {
             throw new IllegalStateException("There is no task view to animate to.");
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index c60e257..facfb9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -126,26 +126,11 @@
 
     @Override
     protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "PortraitStatesTouchController.getTargetState");
-        }
         if (fromState == ALL_APPS && !isDragTowardPositive) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 1");
-            }
             return NORMAL;
         } else if (fromState == OVERVIEW) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 2");
-            }
             return isDragTowardPositive ? OVERVIEW : NORMAL;
         } else if (fromState == NORMAL && isDragTowardPositive) {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS,
-                        "PortraitStatesTouchController.getTargetState 3");
-            }
             return ALL_APPS;
         }
         return fromState;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
index 6271a44..faf5054 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TwoButtonNavbarTouchController.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.views.AllAppsEduView;
 
 /**
@@ -92,7 +93,8 @@
                     mLauncher.getDeviceProfile().isSeascape() == isDragTowardPositive;
             return draggingFromNav ? OVERVIEW : NORMAL;
         } else {
-            return isDragTowardPositive ^ (fromState == OVERVIEW) ? OVERVIEW : NORMAL;
+            LauncherState startState = mStartState != null ? mStartState : fromState;
+            return isDragTowardPositive ^ (startState == OVERVIEW) ? OVERVIEW : NORMAL;
         }
     }
 
@@ -106,8 +108,8 @@
 
     @Override
     protected float getShiftRange() {
-        return mLauncher.getDeviceProfile().isVerticalBarLayout()
-                ? mLauncher.getDragLayer().getWidth() : super.getShiftRange();
+        // Should be in sync with TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT
+        return LayoutUtils.getDefaultSwipeHeight(mLauncher, mLauncher.getDeviceProfile());
     }
 
     @Override
@@ -116,7 +118,7 @@
         long maxAccuracy = (long) (2 * range);
         mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
                 maxAccuracy, animComponent);
-        return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range;
+        return (mLauncher.getDeviceProfile().isSeascape() ? 1 : -1) / range;
     }
 
     @Override
@@ -134,5 +136,6 @@
                 AllAppsEduView.show(mLauncher);
             }
         }
+        mStartState = null;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index feeee50..7f2af6b 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE;
@@ -46,12 +45,15 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+import static com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -78,7 +80,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -102,7 +104,6 @@
 import com.android.quickstep.util.SurfaceTransactionApplier;
 import com.android.quickstep.util.SwipePipToHomeAnimator;
 import com.android.quickstep.util.TransformParams;
-import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -316,7 +317,7 @@
         mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END,
                 this::notifyTransitionCancelled);
 
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT
                             | STATE_SCREENSHOT_VIEW_SHOWN | STATE_CAPTURE_SCREENSHOT,
                     (b) -> mRecentsView.setRunningTaskHidden(!b));
@@ -351,7 +352,6 @@
 
         mRecentsView = activity.getOverviewPanel();
         mRecentsView.setOnPageTransitionEndCallback(null);
-        addLiveTileOverlay();
 
         mStateCallback.setState(STATE_LAUNCHER_PRESENT);
         if (alreadyOnHome) {
@@ -457,7 +457,7 @@
     }
 
     private void onDeferredActivityLaunch() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mActivityInterface.switchRunningTaskViewToScreenshot(
                     null, () -> {
                         mTaskAnimationManager.finishRunningRecentsAnimation(true /* toHome */);
@@ -916,26 +916,15 @@
                 isFling, isCancel);
         float endShift = endTarget.isLauncher ? 1 : 0;
         final float startShift;
-        Interpolator interpolator = DEACCEL;
         if (!isFling) {
             long expectedDuration = Math.abs(Math.round((endShift - currentShift)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
             startShift = currentShift;
-            interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
         } else {
             startShift = Utilities.boundToRange(currentShift - velocity.y
                     * getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
             if (mTransitionDragLength > 0) {
-                if (endTarget == RECENTS && !mDeviceState.isFullyGesturalNavMode()) {
-                    Interpolators.OvershootParams overshoot = new Interpolators.OvershootParams(
-                            startShift, endShift, endShift, endVelocity,
-                            mTransitionDragLength, mContext);
-                    endShift = overshoot.end;
-                    interpolator = overshoot.interpolator;
-                    duration = Utilities.boundToRange(overshoot.duration, MIN_OVERSHOOT_DURATION,
-                            MAX_SWIPE_DURATION);
-                } else {
                     float distanceToTravel = (endShift - currentShift) * mTransitionDragLength;
 
                     // we want the page's snap velocity to approximately match the velocity at
@@ -943,13 +932,9 @@
                     // derivative of the scroll interpolator at zero, ie. 2.
                     long baseDuration = Math.round(Math.abs(distanceToTravel / velocity.y));
                     duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
-
-                    if (endTarget == RECENTS) {
-                        interpolator = OVERSHOOT_1_2;
-                    }
-                }
             }
         }
+        Interpolator interpolator = endTarget == RECENTS ? OVERSHOOT_1_2 : DEACCEL;
 
         if (endTarget.isLauncher) {
             mInputConsumerProxy.enable();
@@ -958,7 +943,7 @@
             duration = Math.max(MIN_OVERSHOOT_DURATION, duration);
         } else if (endTarget == RECENTS) {
             if (mRecentsView != null) {
-                int nearestPage = mRecentsView.getPageNearestToCenterOfScreen();
+                int nearestPage = mRecentsView.getDestinationPage();
                 if (mRecentsView.getNextPage() != nearestPage) {
                     // We shouldn't really scroll to the next page when swiping up to recents.
                     // Only allow settling on the next page if it's nearest to the center.
@@ -1102,8 +1087,8 @@
             homeAnimFactory.playAtomicAnimation(velocityPxPerMs.y);
             mLauncherTransitionController = null;
         } else {
+            AnimatorSet animatorSet = new AnimatorSet();
             ValueAnimator windowAnim = mCurrentShift.animateToValue(start, end);
-            windowAnim.setDuration(duration).setInterpolator(interpolator);
             windowAnim.addUpdateListener(valueAnimator -> {
                 computeRecentsScrollIfInvisible();
             });
@@ -1138,8 +1123,15 @@
                     mGestureState.setState(STATE_END_TARGET_ANIMATION_FINISHED);
                 }
             });
-            windowAnim.start();
-            mRunningWindowAnim = RunningWindowAnim.wrap(windowAnim);
+            animatorSet.play(windowAnim);
+            if (mRecentsView != null && mDp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+                    && mGestureState.getEndTarget() == RECENTS) {
+                animatorSet.play(ObjectAnimator.ofFloat(mRecentsView, RECENTS_GRID_PROGRESS, 1));
+                animatorSet.play(mTaskViewSimulator.gridProgress.animateToValue(0, 1));
+            }
+            animatorSet.setDuration(duration).setInterpolator(interpolator);
+            animatorSet.start();
+            mRunningWindowAnim = RunningWindowAnim.wrap(animatorSet);
         }
     }
 
@@ -1232,13 +1224,6 @@
         });
         anim.addAnimatorListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationStart(Animator animation) {
-                if (mActivity != null) {
-                    removeLiveTileOverlay();
-                }
-            }
-
-            @Override
             public void onAnimationSuccess(Animator animator) {
                 if (mRecentsView != null) {
                     mRecentsView.post(mRecentsView::resetTaskVisuals);
@@ -1259,7 +1244,7 @@
             // In the off chance that the gesture ends before Launcher is started, we should clear
             // the callback here so that it doesn't update with the wrong state
             mActivity.clearRunOnceOnStartCallback();
-            resetLauncherListenersAndOverlays();
+            resetLauncherListeners();
         }
         if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) {
             cancelCurrentAnimation();
@@ -1342,7 +1327,7 @@
         endLauncherTransitionController();
 
         mRecentsView.onGestureAnimationEnd();
-        resetLauncherListenersAndOverlays();
+        resetLauncherListeners();
     }
 
     private void endLauncherTransitionController() {
@@ -1355,13 +1340,12 @@
         }
     }
 
-    private void resetLauncherListenersAndOverlays() {
+    private void resetLauncherListeners() {
         // Reset the callback for deferred activity launches
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             mActivityInterface.setOnDeferredActivityLaunchCallback(null);
         }
         mActivity.getRootView().setOnApplyWindowInsetsListener(null);
-        removeLiveTileOverlay();
     }
 
     private void notifyTransitionCancelled() {
@@ -1378,7 +1362,7 @@
 
     protected void switchToScreenshot() {
         final int runningTaskId = mGestureState.getRunningTaskId();
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             if (mRecentsAnimationController != null) {
                 mRecentsAnimationController.getController().setWillFinishToHome(true);
                 // Update the screenshot of the task
@@ -1447,7 +1431,7 @@
     }
 
     private void finishCurrentTransitionToRecents() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED);
         } else if (!hasTargets() || mRecentsAnimationController == null) {
             // If there are no targets or the animation not started, then there is nothing to finish
@@ -1494,7 +1478,9 @@
         @Override
         public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
                 boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
-            if (mRecentsAnimationTargets.hasTask(task.taskId)) {
+            if (mRecentsView.getRunningTaskIndex() != -1
+                    && mRecentsView.getRunningTaskId() == task.taskId
+                    && mRecentsAnimationTargets.hasTask(task.taskId)) {
                 launchOtherTaskInLiveTileMode(task.taskId, mRecentsAnimationTargets.apps);
             }
             ActivityManagerWrapper.getInstance().unregisterTaskStackListener(
@@ -1506,7 +1492,7 @@
         endLauncherTransitionController();
         mActivityInterface.onSwipeUpToRecentsComplete();
         mRecentsView.onSwipeUpAnimationSuccess();
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mTaskAnimationManager.setLaunchOtherTaskInLiveTileModeHandler(
                     appearedTaskTarget -> {
                         RemoteAnimationTargetCompat[] apps = Arrays.copyOf(
@@ -1583,17 +1569,6 @@
         anim.start();
     }
 
-    private void addLiveTileOverlay() {
-        if (LiveTileOverlay.INSTANCE.attach(mActivity.getRootView().getOverlay())) {
-            mRecentsView.setLiveTileOverlayAttached(true);
-        }
-    }
-
-    private void removeLiveTileOverlay() {
-        LiveTileOverlay.INSTANCE.detach(mActivity.getRootView().getOverlay());
-        mRecentsView.setLiveTileOverlayAttached(false);
-    }
-
     private static boolean isNotInRecents(RemoteAnimationTargetCompat app) {
         return app.isNotInRecents
                 || app.activityType == ACTIVITY_TYPE_HOME;
@@ -1746,19 +1721,14 @@
         if (mWindowTransitionController != null) {
             mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
         }
-        if (mRecentsAnimationTargets != null) {
+        // No need to apply any transform if there is ongoing swipe-pip-to-home animator since
+        // that animator handles the leash solely.
+        if (mRecentsAnimationTargets != null && !mIsSwipingPipToHome) {
             if (mRecentsViewScrollLinked) {
                 mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
             }
             mTaskViewSimulator.apply(mTransformParams);
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mRecentsAnimationTargets != null) {
-            LiveTileOverlay.INSTANCE.update(
-                    mTaskViewSimulator.getCurrentRect(),
-                    mTaskViewSimulator.getCurrentCornerRadius());
-            LiveTileOverlay.INSTANCE.setRotation(
-                    mRecentsView.getPagedViewOrientedState().getDisplayRotation());
-        }
         ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 8b0d782..3f3e5ad 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -187,9 +187,6 @@
 
     @Override
     public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "switchToRecentsIfVisible");
-        }
         Launcher launcher = getVisibleLauncher();
         if (launcher == null) {
             return false;
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 65847f1..192738f 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -59,6 +61,8 @@
 
         super.init(context);
 
+        LIVE_TILE.initialize(context);
+
         // Elevate GPU priority for Quickstep and Remote animations.
         ThreadedRendererCompat.setContextPriority(
                 ThreadedRendererCompat.EGL_CONTEXT_PRIORITY_HIGH_IMG);
diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java
index 1c5dc4c..2f1538b 100644
--- a/quickstep/src/com/android/quickstep/RecentTasksList.java
+++ b/quickstep/src/com/android/quickstep/RecentTasksList.java
@@ -22,10 +22,12 @@
 import android.app.ActivityManager;
 import android.os.Build;
 import android.os.Process;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -51,6 +53,9 @@
 
     // The list change id, increments as the task list changes in the system
     private int mChangeId;
+    // Whether we are currently updating the tasks in the background (up to when the result is
+    // posted back on the main thread)
+    private boolean mLoadingTasksInBackground;
 
     private TaskLoadResult mResultsBg = INVALID_RESULT;
     private TaskLoadResult mResultsUi = INVALID_RESULT;
@@ -64,6 +69,11 @@
         mActivityManagerWrapper.registerTaskStackListener(this);
     }
 
+    @VisibleForTesting
+    public boolean isLoadingTasksInBackground() {
+        return mLoadingTasksInBackground;
+    }
+
     /**
      * Fetches the task keys skipping any local cache.
      */
@@ -83,6 +93,10 @@
      * @return The change id of the current task list
      */
     public synchronized int getTasks(boolean loadKeysOnly, Consumer<ArrayList<Task>> callback) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: keysOnly=" + loadKeysOnly
+                    + " callback=" + callback);
+        }
         final int requestLoadId = mChangeId;
         if (mResultsUi.isValidForRequest(requestLoadId, loadKeysOnly)) {
             // The list is up to date, send the callback on the next frame,
@@ -90,22 +104,38 @@
             if (callback != null) {
                 // Copy synchronously as the changeId might change by next frame
                 ArrayList<Task> result = copyOf(mResultsUi);
-                mMainThreadExecutor.post(() -> callback.accept(result));
+                mMainThreadExecutor.post(() -> {
+                    if (TestProtocol.sDebugTracing) {
+                        Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: no new tasks");
+                    }
+                    callback.accept(result);
+                });
             }
 
             return requestLoadId;
         }
 
         // Kick off task loading in the background
+        mLoadingTasksInBackground = true;
         UI_HELPER_EXECUTOR.execute(() -> {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg start");
+            }
             if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {
                 mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);
             }
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: loading in bg end");
+            }
             TaskLoadResult loadResult = mResultsBg;
             mMainThreadExecutor.execute(() -> {
+                mLoadingTasksInBackground = false;
                 mResultsUi = loadResult;
                 if (callback != null) {
                     ArrayList<Task> result = copyOf(mResultsUi);
+                    if (TestProtocol.sDebugTracing) {
+                        Log.d(TestProtocol.GET_RECENTS_FAILED, "getTasks: callback w/ bg results");
+                    }
                     callback.accept(result);
                 }
             });
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index d47217b..ba24e6a 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -28,6 +28,8 @@
 import android.os.Process;
 import android.os.UserHandle;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.launcher3.icons.IconProvider;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.launcher3.util.MainThreadInitializedObject;
@@ -102,6 +104,14 @@
     }
 
     /**
+     * @return Whether the task list is currently updating in the background
+     */
+    @VisibleForTesting
+    public boolean isLoadingTasksInBackground() {
+        return mTaskList.isLoadingTasksInBackground();
+    }
+
+    /**
      * Finds and returns the task key associated with the given task id.
      *
      * @param callback The callback to receive the task key if it is found or null. This is always
diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
index ca73041..f4b8b62 100644
--- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
+++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java
@@ -94,13 +94,7 @@
         mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
                 dp, mContext, TEMP_RECT,
                 mTaskViewSimulator.getOrientationState().getOrientationHandler());
-
-        if (mDeviceState.isFullyGesturalNavMode()) {
-            // We can drag all the way to the top of the screen.
-            mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
-        } else {
-            mDragLengthFactor = 1 + AnimatorControllerWithResistance.TWO_BUTTON_EXTRA_DRAG_FACTOR;
-        }
+        mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
 
         PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
         mTaskViewSimulator.addAppToOverviewAnim(pa, LINEAR);
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 64d05e1..5668817 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -21,11 +21,11 @@
 import android.app.PictureInPictureParams;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.graphics.Bitmap;
 import android.graphics.Insets;
 import android.graphics.Rect;
-import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IBinder.DeathRecipient;
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.ISplitScreenListener;
+import com.android.systemui.shared.recents.IStartingWindowListener;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.RemoteTransitionCompat;
@@ -516,14 +517,40 @@
     }
 
     @Override
-    public void startIntent(PendingIntent intent, int stage, int position, Bundle options) {
+    public void startIntent(PendingIntent intent, Intent fillInIntent, int stage,
+            int position, Bundle options) {
         if (mSystemUiProxy != null) {
             try {
-                mSystemUiProxy.startIntent(intent, stage, position, options);
+                mSystemUiProxy.startIntent(intent, fillInIntent, stage, position,
+                        options);
             } catch (RemoteException e) {
                 Log.w(TAG, "Failed call startIntent");
             }
         }
     }
 
+    @Override
+    public void removeFromSideStage(int taskId) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.removeFromSideStage(taskId);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call removeFromSideStage");
+            }
+        }
+    }
+
+    /**
+     * Sets listener to get callbacks when launching a task.
+     */
+    @Override
+    public void setStartingWindowListener(IStartingWindowListener listener) {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.setStartingWindowListener(listener);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call setStartingWindowListener", e);
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index 0d2c42e..8636130 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -18,7 +18,7 @@
 
 import static android.view.Surface.ROTATION_0;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
 
@@ -163,7 +163,7 @@
          * @param callback callback to run, after switching to screenshot
          */
         public void endLiveTileMode(@NonNull Runnable callback) {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView();
                 recentsView.switchToScreenshot(
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */, callback));
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 25c0928..17822e6 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -23,14 +23,15 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
 import static com.android.launcher3.anim.Interpolators.clampToProgress;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.statehandlers.DepthController.DEPTH;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.Context;
@@ -138,7 +139,7 @@
         boolean isRunningTask = v.isRunningTask();
         TransformParams params = null;
         TaskViewSimulator tsv = null;
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
+        if (LIVE_TILE.get() && isRunningTask) {
             params = v.getRecentsView().getLiveTileParams();
             tsv = v.getRecentsView().getLiveTileTaskViewSimulator();
         }
@@ -158,8 +159,7 @@
         boolean isQuickSwitch = v.isEndQuickswitchCuj();
         v.setEndQuickswitchCuj(false);
 
-        boolean inLiveTileMode =
-                ENABLE_QUICKSTEP_LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
+        boolean inLiveTileMode = LIVE_TILE.get() && v.getRecentsView().getRunningTaskIndex() != -1;
         final RemoteAnimationTargets targets =
                 new RemoteAnimationTargets(appTargets, wallpaperTargets,
                         inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
@@ -180,6 +180,7 @@
         boolean parallaxCenterAndAdjacentTask =
                 taskIndex != recentsView.getCurrentPage() && !(dp.isTablet
                         && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+        float gridTranslationSecondary = recentsView.getGridTranslationSecondary(taskIndex);
         int startScroll = recentsView.getScrollOffset(taskIndex);
 
         TaskViewSimulator topMostSimulator = null;
@@ -196,6 +197,8 @@
             tsv.setPreview(targets.apps[targets.apps.length - 1]);
             tsv.fullScreenProgress.value = 0;
             tsv.recentsViewScale.value = 1;
+            tsv.gridProgress.value = 1;
+            tsv.gridTranslationSecondary.value = gridTranslationSecondary;
             tsv.setScroll(startScroll);
 
             // Fade in the task during the initial 20% of the animation
@@ -208,6 +211,7 @@
                     AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
             out.setFloat(tsv.recentsViewScale,
                     AnimatedFloat.VALUE, tsv.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
+            out.setFloat(tsv.gridProgress, AnimatedFloat.VALUE, 0, TOUCH_RESPONSE_INTERPOLATOR);
             out.setInt(tsv, TaskViewSimulator.SCROLL, 0, TOUCH_RESPONSE_INTERPOLATOR);
 
             TaskViewSimulator finalTsv = tsv;
@@ -307,7 +311,11 @@
         Animator launcherAnim;
         final AnimatorListenerAdapter windowAnimEndListener;
         if (launcherClosing) {
-            launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
+            Context context = v.getContext();
+            DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+            launcherAnim = dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()
+                    ? ObjectAnimator.ofFloat(recentsView, RecentsView.CONTENT_ALPHA, 0)
+                    : recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
             launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
             launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
 
@@ -336,7 +344,7 @@
             };
         }
         pa.add(launcherAnim);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) {
+        if (LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1) {
             pa.addOnFrameCallback(recentsView::redrawLiveTile);
         }
         anim.play(pa.buildAnim());
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index e4c8b6f..fc805d0 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -21,9 +21,9 @@
 import static android.view.MotionEvent.ACTION_UP;
 
 import static com.android.launcher3.config.FeatureFlags.ASSISTANT_GIVES_LAUNCHER_FOCUS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 
@@ -290,7 +290,13 @@
 
     private void initInputMonitor() {
         disposeEventHandlers();
-        if (mDeviceState.isButtonNavMode() || !SystemUiProxy.INSTANCE.get(this).isActive()) {
+
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.TIS_NO_EVENTS, "initInputMonitor: isButtonMode? "
+                    + mDeviceState.isButtonNavMode());
+        }
+
+        if (mDeviceState.isButtonNavMode()) {
             return;
         }
 
@@ -681,8 +687,7 @@
                     runningComponent != null && runningComponent.equals(homeComponent);
         }
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()
-                && gestureState.getActivityInterface().isInLiveTileMode()) {
+        if (LIVE_TILE.get() && gestureState.getActivityInterface().isInLiveTileMode()) {
             return createOverviewInputConsumer(
                     previousGestureState, gestureState, event, forceOverviewInputConsumer);
         } else if (gestureState.getRunningTask() == null) {
@@ -738,8 +743,7 @@
                 || previousGestureState.isRunningAnimationToLauncher()
                 || (ASSISTANT_GIVES_LAUNCHER_FOCUS.get()
                     && forceOverviewInputConsumer)
-                || (ENABLE_QUICKSTEP_LIVE_TILE.get())
-                    && gestureState.getActivityInterface().isInLiveTileMode()) {
+                || (LIVE_TILE.get()) && gestureState.getActivityInterface().isInLiveTileMode()) {
             return new OverviewInputConsumer(gestureState, activity, mInputMonitorCompat,
                     false /* startingInActivityBounds */);
         } else {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 8f2356c..13f6137 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -24,9 +24,11 @@
 import android.content.Context;
 import android.os.Build;
 import android.util.AttributeSet;
+import android.util.Log;
 
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.statemanager.StateManager.StateListener;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.quickstep.FallbackActivityInterface;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.RecentsActivity;
@@ -120,6 +122,10 @@
         // as well. This tile is never shown as we have setCurrentTaskHidden, but allows use to
         // track the index of the next task appropriately, as if we are switching on any other app.
         if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == mRunningTaskId && !tasks.isEmpty()) {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED,
+                        "FallbackRecentsView.applyLoadPlan: running task is home");
+            }
             // Check if the task list has running task
             boolean found = false;
             for (Task t : tasks) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index aad70c4..cee3363 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -15,7 +15,7 @@
  */
 package com.android.quickstep.inputconsumers;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
 
 import android.view.KeyEvent;
@@ -99,7 +99,7 @@
 
     @Override
     public void onKeyEvent(KeyEvent ev) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mActivity.dispatchKeyEvent(ev);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index deb70e0..7f94839 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
 import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
 
@@ -38,7 +37,6 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.LauncherActivityInterface;
-import com.android.quickstep.SysUINavigationMode;
 import com.android.quickstep.views.RecentsView;
 
 /**
@@ -49,12 +47,6 @@
  */
 public class AnimatorControllerWithResistance {
 
-    /**
-     * How much farther we can drag past overview in 2-button mode, as a factor of the distance
-     * it takes to drag from an app to overview.
-     */
-    public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
-
     private enum RecentsResistanceParams {
         FROM_APP(0.75f, 0.5f, 1f),
         FROM_OVERVIEW(1f, 0.75f, 0.5f);
@@ -161,12 +153,6 @@
         LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect,
                 orientationHandler);
         long distanceToCover = startRect.bottom;
-        boolean isTwoButtonMode = SysUINavigationMode.getMode(params.context) == TWO_BUTTONS;
-        if (isTwoButtonMode) {
-            // We can only drag a small distance past overview, not to the top of the screen.
-            distanceToCover = (long)
-                    ((params.dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
-        }
         PendingAnimation resistAnim = params.resistAnim != null
                 ? params.resistAnim
                 : new PendingAnimation(distanceToCover * 2);
@@ -178,43 +164,35 @@
                 / (params.dp.heightPx - startRect.bottom);
         // This is what the scale would be at the end of the drag if we didn't apply resistance.
         float endScale = params.startScale - prevScaleRate * distanceToCover;
-        final TimeInterpolator scaleInterpolator;
-        if (isTwoButtonMode) {
-            // We are bounded by the distance of the drag, so we don't need to apply resistance.
-            scaleInterpolator = LINEAR;
-        } else {
-            // Create an interpolator that resists the scale so the scale doesn't get smaller than
-            // RECENTS_SCALE_MAX_RESIST.
-            float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
-                    params.startScale, endScale);
-            float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
-                    params.startScale, endScale);
-            scaleInterpolator = t -> {
-                if (t < startResist) {
-                    return t;
-                }
-                float resistProgress = Utilities.getProgress(t, startResist, 1);
-                resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
-                return startResist + resistProgress * (maxResist - startResist);
-            };
-        }
+        // Create an interpolator that resists the scale so the scale doesn't get smaller than
+        // RECENTS_SCALE_MAX_RESIST.
+        float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
+                params.startScale, endScale);
+        float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
+                params.startScale, endScale);
+        final TimeInterpolator scaleInterpolator = t -> {
+            if (t < startResist) {
+                return t;
+            }
+            float resistProgress = Utilities.getProgress(t, startResist, 1);
+            resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
+            return startResist + resistProgress * (maxResist - startResist);
+        };
         resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale,
                 scaleInterpolator);
 
-        if (!isTwoButtonMode) {
-            // Compute where the task view would be based on the end scale, if we didn't translate.
-            RectF endRectF = new RectF(startRect);
-            Matrix temp = new Matrix();
-            temp.setScale(params.resistanceParams.scaleMaxResist,
-                    params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
-            temp.mapRect(endRectF);
-            // Translate such that the task view touches the top of the screen when drag does.
-            float endTranslation = endRectF.top
-                    * orientationHandler.getSecondaryTranslationDirectionFactor()
-                    * params.resistanceParams.translationFactor;
-            resistAnim.addFloat(params.translationTarget, params.translationProperty,
-                    params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
-        }
+        // Compute where the task view would be based on the end scale.
+        RectF endRectF = new RectF(startRect);
+        Matrix temp = new Matrix();
+        temp.setScale(params.resistanceParams.scaleMaxResist,
+                params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
+        temp.mapRect(endRectF);
+        // Translate such that the task view touches the top of the screen when drag does.
+        float endTranslation = endRectF.top
+                * orientationHandler.getSecondaryTranslationDirectionFactor()
+                * params.resistanceParams.translationFactor;
+        resistAnim.addFloat(params.translationTarget, params.translationProperty,
+                params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
 
         return resistAnim;
     }
diff --git a/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
new file mode 100644
index 0000000..60c7add
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/NavigationModeFeatureFlag.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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 com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+
+import android.content.Context;
+
+import com.android.quickstep.SysUINavigationMode;
+
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/** A feature flag that listens to navigation mode changes. */
+public class NavigationModeFeatureFlag implements
+        SysUINavigationMode.NavigationModeChangeListener {
+
+    public static final NavigationModeFeatureFlag LIVE_TILE = new NavigationModeFeatureFlag(
+            ENABLE_QUICKSTEP_LIVE_TILE::get, mode -> mode.hasGestures);
+
+    private final Supplier<Boolean> mBasePredicate;
+    private final Predicate<SysUINavigationMode.Mode> mModePredicate;
+    private boolean mSupported;
+
+    private NavigationModeFeatureFlag(Supplier<Boolean> basePredicate,
+            Predicate<SysUINavigationMode.Mode> modePredicate) {
+        mBasePredicate = basePredicate;
+        mModePredicate = modePredicate;
+    }
+
+    public boolean get() {
+        return mBasePredicate.get() && mSupported;
+    }
+
+    public void initialize(Context context) {
+        onNavigationModeChanged(SysUINavigationMode.INSTANCE.get(context).getMode());
+        SysUINavigationMode.INSTANCE.get(context).addModeChangeListener(this);
+    }
+
+    @Override
+    public void onNavigationModeChanged(SysUINavigationMode.Mode newMode) {
+        mSupported = mModePredicate.test(newMode);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index edce194..9537247 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -15,15 +15,17 @@
  */
 package com.android.quickstep.util;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
 import static com.android.quickstep.util.RecentsOrientedState.preDisplayRotation;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.animation.TimeInterpolator;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -34,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.PagedOrientationHandler;
@@ -73,6 +76,7 @@
 
     @NonNull
     private RecentsOrientedState mOrientationState;
+    private final boolean mIsRecentsRtl;
 
     private final Rect mTaskRect = new Rect();
     private boolean mDrawsBelowRecents;
@@ -93,17 +97,21 @@
     private final FullscreenDrawParams mCurrentFullscreenParams;
     public final AnimatedFloat taskPrimaryTranslation = new AnimatedFloat();
     public final AnimatedFloat taskSecondaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat gridTranslationSecondary = new AnimatedFloat();
 
     // RecentsView properties
     public final AnimatedFloat recentsViewScale = new AnimatedFloat();
     public final AnimatedFloat fullScreenProgress = new AnimatedFloat();
     public final AnimatedFloat recentsViewSecondaryTranslation = new AnimatedFloat();
+    public final AnimatedFloat gridProgress = new AnimatedFloat();
     private final ScrollState mScrollState = new ScrollState();
 
     // Cached calculations
     private boolean mLayoutValid = false;
     private boolean mScrollValid = false;
     private int mOrientationStateId;
+    private final int mTaskThumbnailPadding;
+    private final int mRowSpacing;
 
     public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) {
         mContext = context;
@@ -113,6 +121,10 @@
         mOrientationState.setGestureActive(true);
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mOrientationStateId = mOrientationState.getStateId();
+        Resources resources = context.getResources();
+        mIsRecentsRtl = mOrientationState.getOrientationHandler().getRecentsRtlSetting(resources);
+        mTaskThumbnailPadding = (int) resources.getDimension(R.dimen.task_thumbnail_top_margin);
+        mRowSpacing = (int) resources.getDimension(R.dimen.recents_row_spacing);
     }
 
     /**
@@ -277,9 +289,10 @@
             mScrollState.updateInterpolation(mDp, start);
         }
 
-        float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
+        float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
         mCurrentFullscreenParams.setProgress(
-                progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+                fullScreenProgress, recentsViewScale.value, mTaskRect.width(), mDp,
+                mPositionHelper);
 
         // Apply thumbnail matrix
         RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
@@ -291,6 +304,24 @@
         mMatrix.postTranslate(insets.left, insets.top);
         mMatrix.postScale(scale, scale);
 
+        float interpolatedGridProgress = ACCEL_DEACCEL.getInterpolation(gridProgress.value);
+
+        // Apply TaskView matrix: gridProgress
+        final int boxLength = (int) Math.max(taskWidth, taskHeight);
+        float availableHeight =
+                mTaskThumbnailPadding + taskHeight + mSizeStrategy.getOverviewActionsHeight(
+                        mContext);
+        float rowHeight = (availableHeight - mRowSpacing) / 2;
+        float gridScale = rowHeight / (boxLength + mTaskThumbnailPadding);
+        scale = Utilities.mapRange(interpolatedGridProgress, 1f, gridScale);
+        mMatrix.postScale(scale, scale, mIsRecentsRtl ? 0 : taskWidth, 0);
+        float taskWidthDiff = taskWidth * (1 - gridScale);
+        float taskWidthOffset = mIsRecentsRtl ? taskWidthDiff : -taskWidthDiff;
+        mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, taskWidthOffset));
+        mOrientationState.getOrientationHandler().setSecondary(mMatrix, MATRIX_POST_TRANSLATE,
+                Utilities.mapRange(interpolatedGridProgress, 0, gridTranslationSecondary.value));
+
         // Apply TaskView matrix: translate, scroll
         mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
         mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
@@ -323,7 +354,7 @@
                 .withCornerRadius(getCurrentCornerRadius())
                 .withShadowRadius(app.isTranslucent ? 0 : params.getShadowRadius());
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
+        if (LIVE_TILE.get() && params.getRecentsSurface() != null) {
             // When relativeLayer = 0, it reverts the surfaces back to the original order.
             builder.withRelativeLayerTo(params.getRecentsSurface(),
                     mDrawsBelowRecents ? Integer.MIN_VALUE : 0);
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
index 0837300..9af4d30 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.java
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java
@@ -43,9 +43,12 @@
     private float mScrollAlpha = 1;
     private float mContentAlpha = 1;
     private float mVisibilityAlpha = 1;
+    private float mGridProgress = 1;
 
     private boolean mIsRtl;
     private final float mOriginalTranslationX, mOriginalTranslationY;
+    private float mNormalTranslationPrimary;
+    private float mGridTranslationPrimary;
 
     private int mScrollOffset;
 
@@ -100,10 +103,18 @@
             return;
         }
 
-        float shift = Math.min(scrollState.scrollFromEdge, orientationSize);
-        float translation = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
-        orientationHandler.setPrimaryAndResetSecondaryTranslate(
-                this, translation, mOriginalTranslationX, mOriginalTranslationY);
+        float shift;
+        if (mIsRtl) {
+            shift = Math.min(scrollState.scrollFromEdge, orientationSize);
+        } else {
+            shift = Math.min(scrollState.scrollFromEdge,
+                    orientationSize + getGridTrans(mGridTranslationPrimary))
+                    - getGridTrans(mGridTranslationPrimary);
+        }
+        mNormalTranslationPrimary = mIsRtl ? (mScrollOffset - shift) : (mScrollOffset + shift);
+        applyPrimaryTranslation();
+        orientationHandler.getSecondaryViewTranslate().set(this,
+                orientationHandler.getSecondaryValue(mOriginalTranslationX, mOriginalTranslationY));
         mScrollAlpha = 1 - shift / orientationSize;
         updateAlpha();
     }
@@ -111,6 +122,48 @@
     private void updateAlpha() {
         final float alpha = mScrollAlpha * mContentAlpha * mVisibilityAlpha;
         setAlpha(alpha);
-        setClickable(alpha == 1);
+        setClickable(Math.min(alpha, 1) == 1);
+    }
+
+    public void setGridTranslationPrimary(float gridTranslationPrimary) {
+        mGridTranslationPrimary = gridTranslationPrimary;
+        applyPrimaryTranslation();
+    }
+
+    public float getScrollAdjustment() {
+        float scrollAdjustment = 0;
+        if (mGridProgress > 0) {
+            scrollAdjustment += mGridTranslationPrimary;
+        }
+        return scrollAdjustment;
+    }
+
+    public float getOffsetAdjustment() {
+        return getScrollAdjustment();
+    }
+
+    /**
+     * Moves ClearAllButton between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        mGridProgress = gridProgress;
+        applyPrimaryTranslation();
+    }
+
+    private void applyPrimaryTranslation() {
+        RecentsView recentsView = getRecentsView();
+        if (recentsView == null) {
+            return;
+        }
+
+        PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
+        orientationHandler.getPrimaryViewTranslate().set(this,
+                mNormalTranslationPrimary + getGridTrans(mGridTranslationPrimary));
+    }
+
+    private float getGridTrans(float endTranslation) {
+        return mGridProgress > 0 ? endTranslation : 0;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 52a7466..d99f707 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -16,14 +16,14 @@
 package com.android.quickstep.views;
 
 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
+import static com.android.launcher3.LauncherState.CLEAR_ALL_BUTTON;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS;
 import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
@@ -95,7 +95,7 @@
     public void startHome() {
         Runnable onReachedHome = () -> mActivity.getStateManager().goToState(NORMAL, false);
         OverviewToHomeAnim overviewToHomeAnim = new OverviewToHomeAnim(mActivity, onReachedHome);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             switchToScreenshot(null,
                     () -> finishRecentsAnimation(true /* toRecents */,
                             () -> overviewToHomeAnim.animateWithVelocity(0)));
@@ -169,7 +169,7 @@
         if (enabled) {
             LauncherState state = mActivity.getStateManager().getState();
             boolean hasClearAllButton = (state.getVisibleElements(mActivity)
-                    & OVERVIEW_BUTTONS) != 0;
+                    & CLEAR_ALL_BUTTON) != 0;
             setDisallowScrollToClearAll(!hasClearAllButton);
         }
     }
diff --git a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java b/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
deleted file mode 100644
index 8210ab0..0000000
--- a/quickstep/src/com/android/quickstep/views/LiveTileOverlay.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package com.android.quickstep.views;
-
-import static android.view.Surface.ROTATION_0;
-import static android.view.Surface.ROTATION_180;
-import static android.view.Surface.ROTATION_270;
-import static android.view.Surface.ROTATION_90;
-
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.Drawable;
-import android.util.FloatProperty;
-import android.view.ViewOverlay;
-
-import com.android.launcher3.anim.Interpolators;
-import com.android.quickstep.util.RecentsOrientedState.SurfaceRotation;
-
-public class LiveTileOverlay extends Drawable {
-
-    private static final long ICON_ANIM_DURATION = 120;
-
-    private static final FloatProperty<LiveTileOverlay> PROGRESS =
-            new FloatProperty<LiveTileOverlay>("progress") {
-                @Override
-                public void setValue(LiveTileOverlay liveTileOverlay, float progress) {
-                    liveTileOverlay.setIconAnimationProgress(progress);
-                }
-
-                @Override
-                public Float get(LiveTileOverlay liveTileOverlay) {
-                    return liveTileOverlay.mIconAnimationProgress;
-                }
-            };
-
-    public static final LiveTileOverlay INSTANCE = new LiveTileOverlay();
-
-    private final Paint mPaint = new Paint();
-    private final RectF mCurrentRect = new RectF();
-    private final Rect mBoundsRect = new Rect();
-
-    private @SurfaceRotation int mRotation = ROTATION_0;
-
-    private float mCornerRadius;
-    private Drawable mIcon;
-    private Animator mIconAnimator;
-
-    private float mIconAnimationProgress = 0f;
-    private boolean mIsAttached;
-
-    private LiveTileOverlay() {
-        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-    }
-
-    public void update(RectF currentRect, float cornerRadius) {
-        invalidateSelf();
-
-        mCurrentRect.set(currentRect);
-        mCornerRadius = cornerRadius;
-
-        mCurrentRect.roundOut(mBoundsRect);
-        setBounds(mBoundsRect);
-        invalidateSelf();
-    }
-
-    public void update(float left, float top, float right, float bottom) {
-        mCurrentRect.set(left, top, right, bottom);
-    }
-
-    public void setRotation(@SurfaceRotation int rotation) {
-        mRotation = rotation;
-    }
-
-    public void setIcon(Drawable icon) {
-        mIcon = icon;
-    }
-
-    // TODO: consider cleaning this up and drawing icon in another way. Previously we place app
-    // below launcher during the initial swipe up and render the icon in this live tile overlay.
-    // However, this resulted in a bunch of touch input issues caused by Launcher getting the input
-    // events during transition (to overview / to another app (quick switch). So now our new
-    // solution places app on top in live tile until it fully settles in Overview.
-    public void startIconAnimation() {
-        if (mIconAnimator != null) {
-            mIconAnimator.cancel();
-        }
-        // This animator must match the icon part of {@link TaskView#FOCUS_TRANSITION} animation.
-        mIconAnimator = ObjectAnimator.ofFloat(this, PROGRESS, 1);
-        mIconAnimator.setDuration(ICON_ANIM_DURATION).setInterpolator(LINEAR);
-        mIconAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mIconAnimator = null;
-            }
-        });
-        mIconAnimator.start();
-    }
-
-    public float cancelIconAnimation() {
-        if (mIconAnimator != null) {
-            mIconAnimator.cancel();
-        }
-        return mIconAnimationProgress;
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        canvas.drawRoundRect(mCurrentRect, mCornerRadius, mCornerRadius, mPaint);
-        if (mIcon != null && mIconAnimationProgress > 0f) {
-            canvas.save();
-            float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, 0f,
-                    1f).getInterpolation(mIconAnimationProgress);
-
-            int iconRadius = mIcon.getBounds().width() / 2;
-            float dx = 0;
-            float dy = 0;
-
-            switch (mRotation) {
-                case ROTATION_0:
-                    dx = mCurrentRect.centerX() - iconRadius * scale;
-                    dy = mCurrentRect.top - iconRadius * scale;
-                    break;
-                case ROTATION_90:
-                    dx = mCurrentRect.right - iconRadius * scale;
-                    dy = mCurrentRect.centerY() - iconRadius * scale;
-                    break;
-                case ROTATION_270:
-                    dx = mCurrentRect.left - iconRadius * scale;
-                    dy = mCurrentRect.centerY() - iconRadius * scale;
-                    break;
-                case ROTATION_180:
-                    dx = mCurrentRect.centerX() - iconRadius * scale;
-                    dy = mCurrentRect.bottom - iconRadius * scale;
-                    break;
-            }
-
-            int rotationDegrees = mRotation * 90;
-            if (mRotation == ROTATION_90 || mRotation == ROTATION_270) {
-                canvas.rotate(rotationDegrees, dx + iconRadius, dy + iconRadius);
-            }
-            canvas.translate(dx, dy);
-            canvas.scale(scale, scale);
-            mIcon.draw(canvas);
-            canvas.restore();
-        }
-    }
-
-    @Override
-    public void setAlpha(int i) { }
-
-    @Override
-    public void setColorFilter(ColorFilter colorFilter) { }
-
-    @Override
-    public int getOpacity() {
-        return PixelFormat.TRANSLUCENT;
-    }
-
-    public boolean attach(ViewOverlay overlay) {
-        if (overlay != null && !mIsAttached) {
-            overlay.add(this);
-            mIsAttached = true;
-            return true;
-        }
-
-        return false;
-    }
-
-    public void detach(ViewOverlay overlay) {
-        if (overlay != null) {
-            overlay.remove(this);
-            mIsAttached = false;
-        }
-    }
-
-    private void setIconAnimationProgress(float progress) {
-        mIconAnimationProgress = progress;
-        invalidateSelf();
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 248fa46..f20ca82 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -35,7 +35,6 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
@@ -43,6 +42,7 @@
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_RECENTS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
@@ -72,6 +72,7 @@
 import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
+import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
@@ -104,9 +105,11 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StatefulActivity;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.PagedOrientationHandler.CurveProperties;
 import com.android.launcher3.util.DynamicResource;
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.OverScroller;
 import com.android.launcher3.util.ResourceBasedOverride.Overrides;
@@ -239,6 +242,19 @@
                 }
             };
 
+    public static final FloatProperty<RecentsView> RECENTS_GRID_PROGRESS =
+            new FloatProperty<RecentsView>("recentsGrid") {
+                @Override
+                public void setValue(RecentsView view, float gridProgress) {
+                    view.setGridProgress(gridProgress);
+                }
+
+                @Override
+                public Float get(RecentsView view) {
+                    return view.mGridProgress;
+                }
+            };
+
     protected final RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
@@ -264,6 +280,7 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
+    private final int mRowSpacing;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
@@ -285,6 +302,9 @@
 
     private float mAdjacentPageOffset = 0;
     private float mTaskViewsSecondaryTranslation = 0;
+    // Progress from 0 to 1 where 0 is a carousel and 1 is a 2 row grid.
+    private float mGridProgress = 0;
+    private boolean mShowAsGrid;
 
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -402,7 +422,6 @@
     private boolean mShowEmptyMessage;
     private OnEmptyMessageUpdatedListener mOnEmptyMessageUpdatedListener;
     private Layout mEmptyTextLayout;
-    private boolean mLiveTileOverlayAttached;
 
     // Keeps track of the index where the first TaskView should be
     private int mTaskViewStartIndex = 0;
@@ -450,6 +469,7 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        mRowSpacing = (int) getResources().getDimension(R.dimen.recents_row_spacing);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -479,6 +499,11 @@
         mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
         mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
         mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
+
+        mShowAsGrid =
+                mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
+        mActivity.addOnDeviceProfileChangeListener(newDp ->
+                mShowAsGrid = newDp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
     }
 
     public OverScroller getScroller() {
@@ -537,7 +562,7 @@
     @Override
     protected void onWindowVisibilityChanged(int visibility) {
         super.onWindowVisibilityChanged(visibility);
-        if (visibility != VISIBLE && ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (visibility != VISIBLE && LIVE_TILE.get()) {
             finishRecentsAnimation(true /* toRecents */, null);
         }
         updateTaskStackListenerState();
@@ -640,8 +665,22 @@
     }
 
     public boolean isTaskViewVisible(TaskView tv) {
-        // For now, just check if it's the active task or an adjacent task
-        return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
+        if (mShowAsGrid) {
+            int screenStart = mOrientationHandler.getPrimaryScroll(this);
+            int screenEnd = screenStart + mOrientationHandler.getMeasuredSize(this);
+            return isTaskViewWithinBounds(tv, screenStart, screenEnd);
+        } else {
+            // For now, just check if it's the active task or an adjacent task
+            return Math.abs(indexOfChild(tv) - getNextPage()) <= 1;
+        }
+    }
+
+    private boolean isTaskViewWithinBounds(TaskView tv, int start, int end) {
+        int taskStart = mOrientationHandler.getChildStart(tv) + (int) tv.getOffsetAdjustment();
+        int taskSize = (int) (mOrientationHandler.getMeasuredSize(tv) * tv.getSizeAdjustment());
+        int taskEnd = taskStart + taskSize;
+        return (taskStart >= start && taskStart <= end) || (taskEnd >= start
+                && taskEnd <= end);
     }
 
     public TaskView getTaskView(int taskId) {
@@ -703,10 +742,21 @@
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
 
-        TaskView taskView = getCurrentPageTaskView();
-        if (taskView != null && taskView.offerTouchToChildren(ev)) {
-            // Keep consuming events to pass to delegate
-            return true;
+        if (mShowAsGrid) {
+            int taskCount = getTaskViewCount();
+            for (int i = 0; i < taskCount; i++) {
+                TaskView taskView = getTaskViewAt(i);
+                if (isTaskViewVisible(taskView) && taskView.offerTouchToChildren(ev)) {
+                    // Keep consuming events to pass to delegate
+                    return true;
+                }
+            }
+        } else {
+            TaskView taskView = getCurrentPageTaskView();
+            if (taskView != null && taskView.offerTouchToChildren(ev)) {
+                // Keep consuming events to pass to delegate
+                return true;
+            }
         }
 
         final int x = (int) ev.getX();
@@ -758,6 +808,11 @@
     }
 
     @Override
+    protected boolean snapToPageInFreeScroll() {
+        return !mShowAsGrid;
+    }
+
+    @Override
     protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
         // Enables swiping to the left or right only if the task overlay is not modal.
         if (!isModal()) {
@@ -769,6 +824,12 @@
     }
 
     protected void applyLoadPlan(ArrayList<Task> tasks) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskCount=" + tasks.size());
+            for (Task t : tasks) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "\t" + t);
+            }
+        }
         if (mPendingAnimation != null) {
             mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
             return;
@@ -808,8 +869,8 @@
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
             taskView.bind(task, mOrientationState);
-            taskView.updateTaskSize(!taskView.hasTaskId(mRunningTaskId));
         }
+        updateTaskSize();
 
         if (mNextPage == INVALID_PAGE) {
             // Set the current page to the running task, but not if settling on new task.
@@ -830,12 +891,21 @@
         resetTaskVisuals();
         onTaskStackUpdated();
         updateEnabledOverlays();
+
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "applyLoadPlan: taskViewCount="
+                    + getTaskViewCount());
+        }
     }
 
     private boolean isModal() {
         return mTaskModalness > 0;
     }
 
+    public boolean isLoadingTasks() {
+        return mModel.isLoadingTasksInBackground();
+    }
+
     private void removeTasksViewsAndClearAllButton() {
         for (int i = getTaskViewCount() - 1; i >= 0; i--) {
             removeView(getTaskViewAt(i));
@@ -846,6 +916,12 @@
     }
 
     public int getTaskViewCount() {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getTaskViewCount:"
+                    + " numChildren=" + getChildCount()
+                    + " start=" + mTaskViewStartIndex
+                    + " clearAll=" + indexOfChild(mClearAllButton));
+        }
         int taskViewCount = getChildCount() - mTaskViewStartIndex;
         if (indexOfChild(mClearAllButton) != -1) {
             taskViewCount--;
@@ -868,7 +944,7 @@
                 taskView.setModalness(mTaskModalness);
             }
         }
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             // Since we reuse the same mLiveTileTaskViewSimulator in the RecentsView, we need
             // to reset the params after it settles in Overview from swipe up so that we don't
             // render with obsolete param values.
@@ -876,10 +952,6 @@
             mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
             mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
             mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
-
-            // Reset the live tile rect
-            DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-            LiveTileOverlay.INSTANCE.update(0, 0, deviceProfile.widthPx, deviceProfile.heightPx);
         }
         if (mRunningTaskTileHidden) {
             setRunningTaskHidden(mRunningTaskTileHidden);
@@ -899,12 +971,8 @@
     public void setFullscreenProgress(float fullscreenProgress) {
         mFullscreenProgress = fullscreenProgress;
         int taskCount = getTaskViewCount();
-        float accumulatedTranslationX = 0;
         for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = getTaskViewAt(i);
-            taskView.setFullscreenProgress(mFullscreenProgress);
-            taskView.setAccumulatedTranslationX(accumulatedTranslationX);
-            accumulatedTranslationX += taskView.getFullscreenTranslationX();
+            getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
         }
 
         // Fade out the actions view quickly (0.1 range)
@@ -941,11 +1009,22 @@
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
         // Force TaskView to update size from thumbnail
+        updateTaskSize();
+    }
+
+    /**
+     * Updates TaskView scaling and translation required to support variable width.
+     */
+    private void updateTaskSize() {
+        float accumulatedTranslationX = 0;
         final int taskCount = getTaskViewCount();
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = getTaskViewAt(i);
-            taskView.updateTaskSize(!taskView.hasTaskId(mRunningTaskId));
+            taskView.updateTaskSize();
+            taskView.setAccumulatedFullscreenTranslationX(accumulatedTranslationX);
+            accumulatedTranslationX += taskView.getFullscreenTranslationX();
         }
+        updateGridProperties();
     }
 
     public void getTaskSize(Rect outRect) {
@@ -983,7 +1062,7 @@
         mModel.getThumbnailCache().getHighResLoadingState().setFlingingFast(isFlingingFast);
 
         mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
+        if (LIVE_TILE.get() && mEnableDrawingLiveTile
                 && mLiveTileParams.getTargetSet() != null) {
             redrawLiveTile();
         }
@@ -1010,6 +1089,31 @@
         }
     }
 
+    @Override
+    protected int getDestinationPage(int scaledScroll) {
+        if (mGridProgress == 0) {
+            return super.getDestinationPage(scaledScroll);
+        }
+
+        final int childCount = getChildCount();
+        if (mPageScrolls == null || childCount != mPageScrolls.length) {
+            return -1;
+        }
+
+        // When in grid, return the page which scroll is closest to screenStart instead of page
+        // nearest to center of screen.
+        int minDistanceFromScreenStart = Integer.MAX_VALUE;
+        int minDistanceFromScreenStartIndex = -1;
+        for (int i = 0; i < childCount; ++i) {
+            int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
+            if (distanceFromScreenStart < minDistanceFromScreenStart) {
+                minDistanceFromScreenStart = distanceFromScreenStart;
+                minDistanceFromScreenStartIndex = i;
+            }
+        }
+        return minDistanceFromScreenStartIndex;
+    }
+
     /**
      * Iterates through all the tasks, and loads the associated task data for newly visible tasks,
      * and unloads the associated task data for tasks that are no longer visible.
@@ -1021,17 +1125,35 @@
             return;
         }
 
-        int centerPageIndex = getPageNearestToCenterOfScreen();
-        int numChildren = getChildCount();
-        int lower = Math.max(0, centerPageIndex - 2);
-        int upper = Math.min(centerPageIndex + 2, numChildren - 1);
+        int lower = 0;
+        int upper = 0;
+        int visibleStart = 0;
+        int visibleEnd = 0;
+        if (mShowAsGrid) {
+            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;
+        } else {
+            int centerPageIndex = getPageNearestToCenterOfScreen();
+            int numChildren = getChildCount();
+            lower = Math.max(0, centerPageIndex - 2);
+            upper = Math.min(centerPageIndex + 2, numChildren - 1);
+        }
 
         // Update the task data for the in/visible children
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = getTaskViewAt(i);
             Task task = taskView.getTask();
             int index = indexOfChild(taskView);
-            boolean visible = lower <= index && index <= upper;
+            boolean visible;
+            if (mShowAsGrid) {
+                visible = isTaskViewWithinBounds(taskView, visibleStart, visibleEnd);
+            } else {
+                visible = lower <= index && index <= upper;
+            }
             if (visible) {
                 if (task == mTmpRunningTask) {
                     // Skip loading if this is the task that we are animating into
@@ -1106,6 +1228,10 @@
         }
     }
 
+    public int getRunningTaskId() {
+        return mRunningTaskId;
+    }
+
     public @Nullable TaskView getRunningTaskView() {
         return getTaskView(mRunningTaskId);
     }
@@ -1158,10 +1284,7 @@
      */
     public void onSwipeUpAnimationSuccess() {
         if (getRunningTaskView() != null) {
-            float startProgress = ENABLE_QUICKSTEP_LIVE_TILE.get() && mLiveTileOverlayAttached
-                    ? LiveTileOverlay.INSTANCE.cancelIconAnimation()
-                    : 0f;
-            animateUpRunningTaskIconScale(startProgress);
+            animateUpRunningTaskIconScale(0f);
         }
         setSwipeDownShouldLaunchApp(true);
     }
@@ -1220,12 +1343,16 @@
         setOnScrollChangeListener(null);
         setEnableFreeScroll(true);
         setEnableDrawingLiveTile(true);
-        if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (!LIVE_TILE.get()) {
             setRunningTaskViewShowScreenshot(true);
         }
         setRunningTaskHidden(false);
         animateUpRunningTaskIconScale();
-        animateActionsViewIn();
+
+        // TODO: This should be tied to whether there is a focus app on overview.
+        if (!mShowAsGrid) {
+            animateActionsViewIn();
+        }
     }
 
     /**
@@ -1254,7 +1381,6 @@
             // gesture and the task list is loaded and applied
             mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
             taskView.bind(mTmpRunningTask, mOrientationState);
-            taskView.updateTaskSize(false);
 
             // Measure and layout immediately so that the scroll values is updated instantly
             // as the user might be quick-switching
@@ -1268,6 +1394,8 @@
         setCurrentPage(getRunningTaskIndex());
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
+        // Update task size after setting current task.
+        updateTaskSize();
 
         // Reload the task list
         mTaskListChangeId = mModel.getTasks(this::applyLoadPlan);
@@ -1307,7 +1435,7 @@
     }
 
     private void setRunningTaskViewShowScreenshot(boolean showScreenshot) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             TaskView runningTaskView = getRunningTaskView();
             if (runningTaskView != null) {
                 runningTaskView.setShowScreenshot(showScreenshot);
@@ -1376,6 +1504,166 @@
         }
     }
 
+    /**
+     * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
+     * layout.
+     * This method only calculates the potential position and depends on {@link #setGridProgress} to
+     * apply the actual scaling and translation.
+     */
+    private void updateGridProperties() {
+        int taskCount = getTaskViewCount();
+        if (taskCount == 0) {
+            return;
+        }
+
+        final int boxLength = Math.max(mTaskWidth, mTaskHeight);
+
+        float availableHeight =
+                mTaskTopMargin + mTaskHeight + mSizeStrategy.getOverviewActionsHeight(mContext);
+        float rowHeight = (availableHeight - mRowSpacing) / 2;
+        float gridScale = rowHeight / (boxLength + mTaskTopMargin);
+
+        TaskView firstTask = getTaskViewAt(0);
+        float firstTaskWidthOffset;
+        if (mIsRtl) {
+            // Move the first task to the right edge.
+            firstTaskWidthOffset = mTaskWidth - firstTask.getLayoutParams().width * gridScale;
+        } else {
+            // Move the first task to the left edge.
+            firstTaskWidthOffset = -firstTask.getLayoutParams().width * (1 - gridScale);
+        }
+
+        int topRowWidth = 0;
+        int bottomRowWidth = 0;
+        float topAccumulatedTranslationX = 0;
+        float bottomAccumulatedTranslationX = 0;
+        IntSet topSet = new IntSet();
+        float[] gridTranslations = new float[taskCount];
+        for (int i = 0; i < taskCount; i++) {
+            TaskView taskView = getTaskViewAt(i);
+            taskView.setGridScale(gridScale);
+
+            float taskWidthDiff = mTaskWidth - taskView.getLayoutParams().width * gridScale;
+            float taskWidthOffset = mIsRtl ? taskWidthDiff : -taskWidthDiff;
+            // Visually we want to move all task by firstTaskWidthOffset, but calculate page scroll
+            // according to right edge (or left in nonRtl) of TaskView.
+            gridTranslations[i] = firstTaskWidthOffset - taskWidthOffset;
+            taskView.setGridOffsetTranslationX(taskWidthOffset);
+
+            // Off-set gap due to task scaling.
+            float widthDiff = taskView.getLayoutParams().width * (1 - gridScale);
+            float gridScaleTranslationX = mIsRtl ? widthDiff : -widthDiff;
+            gridTranslations[i] += gridScaleTranslationX;
+
+            // Visible offset caused by having scaling pivot on top-right.
+            taskView.setNonRtlVisibleOffset(mIsRtl ? 0 : widthDiff);
+
+            if (topRowWidth <= bottomRowWidth) {
+                gridTranslations[i] += topAccumulatedTranslationX;
+                topRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
+                topSet.add(i);
+
+                taskView.setGridTranslationY(0);
+
+                // Move horizontally into empty space.
+                float widthOffset = 0;
+                for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
+                    widthOffset += getTaskViewAt(j).getLayoutParams().width * gridScale
+                            + mPageSpacing;
+                }
+
+                float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+                gridTranslations[i] += gridTranslationX;
+                topAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
+                bottomAccumulatedTranslationX += gridScaleTranslationX;
+            } else {
+                gridTranslations[i] += bottomAccumulatedTranslationX;
+                bottomRowWidth += taskView.getLayoutParams().width * gridScale + mPageSpacing;
+
+                // Move into bottom row.
+                float heightOffset = (boxLength + mTaskTopMargin) * gridScale + mRowSpacing;
+                taskView.setGridTranslationY(heightOffset);
+
+                // Move horizontally into empty space.
+                float widthOffset = 0;
+                for (int j = i - 1; topSet.contains(j); j--) {
+                    widthOffset += getTaskViewAt(j).getLayoutParams().width * gridScale
+                            + mPageSpacing;
+                }
+
+                float gridTranslationX = mIsRtl ? widthOffset : -widthOffset;
+                gridTranslations[i] += gridTranslationX;
+                topAccumulatedTranslationX += gridScaleTranslationX;
+                bottomAccumulatedTranslationX += gridTranslationX + gridScaleTranslationX;
+            }
+        }
+
+        // Use the accumulated translation of the longer row.
+        float clearAllAccumulatedTranslation = mIsRtl ? Math.max(topAccumulatedTranslationX,
+                bottomAccumulatedTranslationX) : Math.min(topAccumulatedTranslationX,
+                bottomAccumulatedTranslationX);
+
+        // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
+        // which is not what we want. Compensate the width difference of the 2 rows in that case.
+        float shorterRowCompensation = 0;
+        if (topRowWidth <= bottomRowWidth) {
+            if (topSet.contains(taskCount - 1)) {
+                shorterRowCompensation = bottomRowWidth - topRowWidth;
+            }
+        } else {
+            if (!topSet.contains(taskCount - 1)) {
+                shorterRowCompensation = topRowWidth - bottomRowWidth;
+            }
+        }
+        float clearAllShorterRowCompensation =
+                mIsRtl ? -shorterRowCompensation : shorterRowCompensation;
+
+        // If the total width is shorter than one task's width, move ClearAllButton further away
+        // accordingly.
+        float clearAllShortTotalCompensation = 0;
+        float longRowWidth = Math.max(topRowWidth, bottomRowWidth);
+        if (longRowWidth < mTaskWidth) {
+            float shortTotalCompensation = mTaskWidth - longRowWidth;
+            clearAllShortTotalCompensation =
+                    mIsRtl ? -shortTotalCompensation : shortTotalCompensation;
+        }
+
+        float clearAllTotalTranslationX = firstTaskWidthOffset + clearAllAccumulatedTranslation
+                + clearAllShorterRowCompensation + clearAllShortTotalCompensation;
+
+        // We need to maintain first task's grid translation at 0, now shift translation of all
+        // the TaskViews to achieve that.
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).setGridTranslationX(gridTranslations[i] - gridTranslations[0]);
+        }
+        mClearAllButton.setGridTranslationPrimary(clearAllTotalTranslationX - gridTranslations[0]);
+
+        setGridProgress(mGridProgress);
+    }
+
+    /**
+     * Moves TaskView and ClearAllButton between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        int taskCount = getTaskViewCount();
+        if (taskCount == 0) {
+            return;
+        }
+
+        if (!mShowAsGrid) {
+            gridProgress = 0;
+        }
+
+        mGridProgress = gridProgress;
+
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).setGridProgress(gridProgress);
+        }
+        mClearAllButton.setGridProgress(gridProgress);
+    }
+
     private void enableLayoutTransitions() {
         if (mLayoutTransition == null) {
             mLayoutTransition = new LayoutTransition();
@@ -1511,7 +1799,7 @@
                 if (animateTaskView) {
                     addDismissedTaskAnimations(taskView, duration, anim);
                 }
-            } else {
+            } else if (!mShowAsGrid) {  // Don't animate other tasks when dismissing in grid for now
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
                 // translates to the left. We need to offset this in some cases:
                 // - In RTL, add page offset to all pages, since we want pages to move to the right
@@ -1553,7 +1841,7 @@
             anim.addOnFrameCallback(this::updateCurveProperties);
         }
 
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && getRunningTaskView() == taskView) {
+        if (LIVE_TILE.get() && getRunningTaskView() == taskView) {
             anim.addOnFrameCallback(() -> {
                 mLiveTileTaskViewSimulator.taskSecondaryTranslation.value =
                         mOrientationHandler.getSecondaryValue(
@@ -1572,7 +1860,7 @@
         mPendingAnimation.addEndListener(new Consumer<Boolean>() {
             @Override
             public void accept(Boolean success) {
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask() && success) {
+                if (LIVE_TILE.get() && taskView.isRunningTask() && success) {
                     finishRecentsAnimation(true /* toHome */, () -> onEnd(success));
                 } else {
                     onEnd(success);
@@ -1604,6 +1892,8 @@
                         startHome();
                     } else {
                         snapToPageImmediately(pageToSnapTo);
+                        // Grid got messed up, reapply.
+                        updateGridProperties();
                     }
                     // Update the layout synchronously so that the position of next view is
                     // immediately available.
@@ -2117,7 +2407,7 @@
             anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex),
                     mOrientationHandler.getPrimaryViewTranslate(), primaryTranslation));
             int runningTaskIndex = recentsView.getRunningTaskIndex();
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get() && runningTaskIndex != -1
+            if (LIVE_TILE.get() && runningTaskIndex != -1
                     && runningTaskIndex != taskIndex) {
                 anim.play(ObjectAnimator.ofFloat(
                         recentsView.getLiveTileTaskViewSimulator().taskPrimaryTranslation,
@@ -2194,7 +2484,7 @@
 
         mPendingAnimation = new PendingAnimation(duration);
         mPendingAnimation.add(anim);
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator);
             mPendingAnimation.addOnFrameCallback(this::redrawLiveTile);
         }
@@ -2206,7 +2496,7 @@
                         tv.notifyTaskLaunchFailed(TAG);
                     }
                 };
-                if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+                if (LIVE_TILE.get()) {
                     finishRecentsAnimation(false /* toRecents */, null);
                     onLaunchResult.accept(true /* success */);
                 } else {
@@ -2316,16 +2606,6 @@
         }
     }
 
-    public void setLiveTileOverlayAttached(boolean liveTileOverlayAttached) {
-        mLiveTileOverlayAttached = liveTileOverlayAttached;
-    }
-
-    public void updateLiveTileIcon(Drawable icon) {
-        if (mLiveTileOverlayAttached) {
-            LiveTileOverlay.INSTANCE.setIcon(icon);
-        }
-    }
-
     public void finishRecentsAnimation(boolean toRecents, Runnable onFinishComplete) {
         if (mRecentsAnimationController == null) {
             if (onFinishComplete != null) {
@@ -2379,17 +2659,16 @@
         boolean pageScrollChanged = super.getPageScrolls(outPageScrolls, layoutChildren,
                 scrollLogic);
 
-        final int taskCount = getTaskViewCount();
         final int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            if (childCount < mTaskViewStartIndex) {
-                continue;
+            View child = getChildAt(i);
+            float scrollDiff = 0;
+            if (child instanceof TaskView) {
+                scrollDiff = ((TaskView) child).getScrollAdjustment();
+            } else if (child instanceof ClearAllButton) {
+                scrollDiff = ((ClearAllButton) child).getScrollAdjustment();
             }
 
-            final TaskView taskView = getTaskViewAt(
-                    Utilities.boundToRange(i, mTaskViewStartIndex, taskCount - 1));
-            float scrollDiff =
-                    taskView.getFullscreenTranslationX() + taskView.getAccumulatedTranslationX();
             if (scrollDiff != 0) {
                 outPageScrolls[i] += scrollDiff;
                 pageScrollChanged = true;
@@ -2400,14 +2679,14 @@
 
     @Override
     protected int getChildOffset(int index) {
-        if (index < mTaskViewStartIndex) {
-            return super.getChildOffset(index);
+        int childOffset = super.getChildOffset(index);
+        View child = getChildAt(index);
+        if (child instanceof TaskView) {
+            childOffset += ((TaskView) child).getOffsetAdjustment();
+        } else if (child instanceof ClearAllButton) {
+            childOffset += ((ClearAllButton) child).getOffsetAdjustment();
         }
-
-        final TaskView taskView = getTaskViewAt(
-                Utilities.boundToRange(index, mTaskViewStartIndex, getTaskViewCount() - 1));
-        return super.getChildOffset(index) + (int) taskView.getFullscreenTranslationX()
-                + (int) taskView.getAccumulatedTranslationX();
+        return childOffset;
     }
 
     @Override
@@ -2416,7 +2695,7 @@
         if (taskView == null) {
             return super.getChildVisibleSize(index);
         }
-        return super.getChildVisibleSize(index) - (int) taskView.getFullscreenTranslationX();
+        return (int) (super.getChildVisibleSize(index) * taskView.getSizeAdjustment());
     }
 
     @Override
@@ -2457,7 +2736,7 @@
     }
 
     /**
-     * @return How many pixels the page is offset on the currently laid out dominant axis.
+     * Returns how many pixels the page is offset on the currently laid out dominant axis.
      */
     public int getScrollOffset(int pageIndex) {
         if (pageIndex == -1) {
@@ -2473,6 +2752,20 @@
         return getScrollForPage(pageIndex) - scroll;
     }
 
+    /**
+     * Returns how many pixels the task is offset on the currently laid out secondary axis
+     * according to {@link #mGridProgress}.
+     */
+    public float getGridTranslationSecondary(int pageIndex) {
+        TaskView taskView = getTaskViewAtByAbsoluteIndex(pageIndex);
+        if (taskView == null) {
+            return 0;
+        }
+
+        return mOrientationHandler.getSecondaryValue(taskView.getGridTranslationX(),
+                taskView.getGridTranslationY());
+    }
+
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
         float degreesRotated;
         if (navbarRotation == 0) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index e21bf76..2315147 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,7 +16,7 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
 
 import android.animation.Animator;
@@ -38,6 +38,7 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.util.Themes;
@@ -113,7 +114,12 @@
         // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
         // which would render the X and Y position set here incorrect
         setPivotX(0);
-        setPivotY(0);
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            // In tablet, set pivotY to original position without mThumbnailTopMargin adjustment.
+            setPivotY(-mThumbnailTopMargin);
+        } else {
+            setPivotY(0);
+        }
         setRotation(pagedOrientationHandler.getDegreesRotated());
         setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
         setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
@@ -177,7 +183,7 @@
         LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
         mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
         menuOptionView.setOnClickListener(view -> {
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (LIVE_TILE.get()) {
                 RecentsView recentsView = mTaskView.getRecentsView();
                 recentsView.switchToScreenshot(null,
                         () -> recentsView.finishRecentsAnimation(true /* toRecents */,
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index f2f4bc1..4c21745 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -19,7 +19,7 @@
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
 
 import android.content.Context;
@@ -319,7 +319,7 @@
 
     public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height,
             float cornerRadius) {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+        if (LIVE_TILE.get()) {
             if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint);
                 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index dfbe6ce..0cf7261 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -35,10 +35,10 @@
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+import static com.android.quickstep.util.NavigationModeFeatureFlag.LIVE_TILE;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -248,8 +248,9 @@
     private IconView mIconView;
     private final DigitalWellBeingToast mDigitalWellBeingToast;
     private float mFullscreenProgress;
-    private float mScaleAtFullscreen = 1;
+    private float mGridProgress;
     private float mFullscreenScale = 1;
+    private float mGridScale = 1;
     private final FullscreenDrawParams mCurrentFullscreenParams;
     private final StatefulActivity mActivity;
 
@@ -262,8 +263,14 @@
     private float mTaskResistanceTranslationY;
     // The following translation variables should only be used in the same orientation as Launcher.
     private float mFullscreenTranslationX;
-    private float mAccumulatedTranslationX;
+    private float mAccumulatedFullscreenTranslationX;
     private float mBoxTranslationY;
+    // The following grid translations scales with mGridProgress.
+    private float mGridTranslationX;
+    private float mGridTranslationY;
+    // Offset translation does not affect scroll calculation.
+    private float mGridOffsetTranslationX;
+    private float mNonRtlVisibleOffset;
 
     private ObjectAnimator mIconAndDimAnimator;
     private float mIconScaleAnimStartProgress = 0;
@@ -301,7 +308,7 @@
             if (getTask() == null) {
                 return;
             }
-            if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+            if (LIVE_TILE.get() && isRunningTask()) {
                 if (!mIsClickableAsLiveTile) {
                     return;
                 }
@@ -552,9 +559,6 @@
             mIconLoadRequest = iconCache.updateIconInBackground(mTask,
                     (task) -> {
                         setIcon(task.icon);
-                        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
-                            getRecentsView().updateLiveTileIcon(task.icon);
-                        }
                         mDigitalWellBeingToast.initialize(mTask);
                     });
         } else {
@@ -722,7 +726,9 @@
 
     @Override
     public void onRecycle() {
-        mFullscreenTranslationX = mAccumulatedTranslationX = mBoxTranslationY = 0f;
+        mFullscreenTranslationX = mAccumulatedFullscreenTranslationX = mGridTranslationX =
+                mGridTranslationY =
+                        mGridOffsetTranslationX = mBoxTranslationY = mNonRtlVisibleOffset = 0f;
         resetViewTransforms();
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
@@ -807,7 +813,7 @@
         super.onLayout(changed, left, top, right, bottom);
         if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
             setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? (right - left) : 0);
-            setPivotY(0);
+            setPivotY(mSnapshotView.getTop());
         } else {
             setPivotX((right - left) * 0.5f);
             setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
@@ -834,9 +840,31 @@
         applyScale();
     }
 
+    public void setGridScale(float gridScale) {
+        mGridScale = gridScale;
+        applyScale();
+    }
+
+    /**
+     * Moves TaskView between carousel and 2 row grid.
+     *
+     * @param gridProgress 0 = carousel; 1 = 2 row grid.
+     */
+    public void setGridProgress(float gridProgress) {
+        mGridProgress = gridProgress;
+        applyTranslationX();
+        applyTranslationY();
+        applyScale();
+    }
+
     private void applyScale() {
-        setScaleX(mFullscreenScale);
-        setScaleY(mFullscreenScale);
+        float scale = 1;
+        float fullScreenProgress = EXAGGERATED_EASE.getInterpolation(mFullscreenProgress);
+        scale *= Utilities.mapRange(fullScreenProgress, 1f, mFullscreenScale);
+        float gridProgress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+        scale *= Utilities.mapRange(gridProgress, 1f, mGridScale);
+        setScaleX(scale);
+        setScaleY(scale);
     }
 
     private void setDismissTranslationX(float x) {
@@ -878,13 +906,66 @@
         return mFullscreenTranslationX;
     }
 
-    public void setAccumulatedTranslationX(float accumulatedTranslationX) {
-        mAccumulatedTranslationX = accumulatedTranslationX;
+    public void setAccumulatedFullscreenTranslationX(float accumulatedFullscreenTranslationX) {
+        mAccumulatedFullscreenTranslationX = accumulatedFullscreenTranslationX;
         applyTranslationX();
     }
 
-    public float getAccumulatedTranslationX() {
-        return mAccumulatedTranslationX;
+    public void setGridTranslationX(float gridTranslationX) {
+        mGridTranslationX = gridTranslationX;
+        applyTranslationX();
+    }
+
+    public float getGridTranslationX() {
+        return mGridTranslationX;
+    }
+
+    public void setGridTranslationY(float gridTranslationY) {
+        mGridTranslationY = gridTranslationY;
+        applyTranslationY();
+    }
+
+    public float getGridTranslationY() {
+        return mGridTranslationY;
+    }
+
+    public void setGridOffsetTranslationX(float gridOffsetTranslationX) {
+        mGridOffsetTranslationX = gridOffsetTranslationX;
+        applyTranslationX();
+    }
+
+    public void setNonRtlVisibleOffset(float nonRtlVisibleOffset) {
+        mNonRtlVisibleOffset = nonRtlVisibleOffset;
+    }
+
+    public float getScrollAdjustment() {
+        float scrollAdjustment = 0;
+        if (mFullscreenProgress > 0) {
+            scrollAdjustment += mFullscreenTranslationX + mAccumulatedFullscreenTranslationX;
+        }
+        if (mGridProgress > 0) {
+            scrollAdjustment += mGridTranslationX;
+        }
+        return scrollAdjustment;
+    }
+
+    public float getOffsetAdjustment() {
+        float offsetAdjustment = getScrollAdjustment();
+        if (mGridProgress > 0) {
+            offsetAdjustment += mGridOffsetTranslationX + mNonRtlVisibleOffset;
+        }
+        return offsetAdjustment;
+    }
+
+    public float getSizeAdjustment() {
+        float sizeAdjustment = 1;
+        if (mFullscreenProgress > 0) {
+            sizeAdjustment *= mFullscreenScale;
+        }
+        if (mGridProgress > 0) {
+            sizeAdjustment *= mGridScale;
+        }
+        return sizeAdjustment;
     }
 
     private void setBoxTranslationY(float boxTranslationY) {
@@ -893,15 +974,20 @@
     }
 
     private void applyTranslationX() {
-        setTranslationX(
-                mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX
-                        + mFullscreenTranslationX + mAccumulatedTranslationX);
+        setTranslationX(mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX
+                + getFullscreenTrans(mFullscreenTranslationX + mAccumulatedFullscreenTranslationX)
+                + getGridTrans(mGridTranslationX + mGridOffsetTranslationX));
     }
 
     private void applyTranslationY() {
         setTranslationY(
                 mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY
-                        + mBoxTranslationY);
+                        + getGridTrans(mGridTranslationY) + mBoxTranslationY);
+    }
+
+    private float getGridTrans(float endTranslation) {
+        float progress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+        return Utilities.mapRange(progress, 0, endTranslation);
     }
 
     public FloatProperty<TaskView> getFillDismissGapTranslationProperty() {
@@ -1057,7 +1143,9 @@
         mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
         getThumbnail().getTaskOverlay().setFullscreenProgress(progress);
 
-        updateTaskScaling();
+        applyTranslationX();
+        applyTranslationY();
+        applyScale();
 
         TaskThumbnailView thumbnail = getThumbnail();
         updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
@@ -1084,41 +1172,53 @@
                 previewPositionHelper);
     }
 
-    void updateTaskSize(boolean variableWidth) {
+    /**
+     * Updates TaskView scaling and translation required to support variable width if enabled, while
+     * ensuring TaskView fits into screen in fullscreen.
+     */
+    void updateTaskSize() {
         ViewGroup.LayoutParams params = getLayoutParams();
-        float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio() : 0f;
-        if (variableWidth && mActivity.getDeviceProfile().isTablet
-                && FeatureFlags.ENABLE_OVERVIEW_GRID.get() && thumbnailRatio != 0f) {
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
             final int thumbnailPadding = (int) getResources().getDimension(
                     R.dimen.task_thumbnail_top_margin);
 
             Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
             int taskWidth = lastComputedTaskSize.width();
             int taskHeight = lastComputedTaskSize.height();
-            int boxLength = Math.max(taskWidth, taskHeight);
 
             int expectedWidth;
             int expectedHeight;
-            if (thumbnailRatio > 1) {
-                expectedWidth = boxLength;
-                expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
+            float thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio() : 0f;
+            if (isRunningTask() || thumbnailRatio == 0f) {
+                expectedWidth = taskWidth;
+                expectedHeight = taskHeight + thumbnailPadding;
             } else {
-                expectedWidth = (int) (boxLength * thumbnailRatio);
-                expectedHeight = boxLength + thumbnailPadding;
+                int boxLength = Math.max(taskWidth, taskHeight);
+                if (thumbnailRatio > 1) {
+                    expectedWidth = boxLength;
+                    expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
+                } else {
+                    expectedWidth = (int) (boxLength * thumbnailRatio);
+                    expectedHeight = boxLength + thumbnailPadding;
+                }
             }
 
             float heightDiff = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
             setBoxTranslationY(heightDiff);
 
+            float fullscreenScale = 1f;
             if (expectedWidth > taskWidth) {
                 // In full screen, expectedWidth should not be larger than taskWidth.
-                mScaleAtFullscreen = taskWidth / (float) expectedWidth;
+                fullscreenScale = taskWidth / (float) expectedWidth;
             } else if (expectedHeight - thumbnailPadding > taskHeight) {
                 // In full screen, expectedHeight should not be larger than taskHeight.
-                mScaleAtFullscreen = taskHeight / (float) (expectedHeight - thumbnailPadding);
-            } else {
-                mScaleAtFullscreen = 1f;
+                fullscreenScale = taskHeight / (float) (expectedHeight - thumbnailPadding);
             }
+            setFullscreenScale(fullscreenScale);
+
+            float widthDiff = params.width * (1 - mFullscreenScale);
+            setFullscreenTranslationX(
+                    getLayoutDirection() == LAYOUT_DIRECTION_RTL ? -widthDiff : widthDiff);
 
             if (params.width != expectedWidth || params.height != expectedHeight) {
                 params.width = expectedWidth;
@@ -1127,35 +1227,16 @@
             }
         } else {
             setBoxTranslationY(0);
+            setFullscreenTranslationX(0);
+            setFullscreenScale(1);
             if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
                 params.width = ViewGroup.LayoutParams.MATCH_PARENT;
                 params.height = ViewGroup.LayoutParams.MATCH_PARENT;
                 setLayoutParams(params);
             }
         }
-        updateTaskScaling();
     }
 
-    private void updateTaskScaling() {
-        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
-            ViewGroup.LayoutParams params = getLayoutParams();
-            if (params.width == ViewGroup.LayoutParams.MATCH_PARENT
-                    || params.height == ViewGroup.LayoutParams.MATCH_PARENT) {
-                // Snapshot is not loaded yet, skip.
-                return;
-            }
-
-            float progress = EXAGGERATED_EASE.getInterpolation(mFullscreenProgress);
-            setFullscreenScale(Utilities.mapRange(progress, 1f, mScaleAtFullscreen));
-
-            float widthDiff = params.width * (1 - mFullscreenScale);
-            setFullscreenTranslationX(getFullscreenTrans(
-                    getLayoutDirection() == LAYOUT_DIRECTION_RTL ? -widthDiff : widthDiff));
-        } else {
-            setFullscreenScale(1);
-            setFullscreenTranslationX(0);
-        }
-    }
 
     private float getFullscreenTrans(float endTranslation) {
         float progress = ACCEL_DEACCEL.getInterpolation(mFullscreenProgress);
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index 2e7e6e0..713fd07 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -42,6 +42,7 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.os.RemoteException;
+import android.util.Log;
 
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -55,6 +56,7 @@
 import com.android.launcher3.tapl.OverviewTask;
 import com.android.launcher3.tapl.TestHelpers;
 import com.android.launcher3.testcomponent.TestCommandReceiver;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.rule.FailureWatcher;
 import com.android.quickstep.views.RecentsView;
@@ -172,9 +174,15 @@
 
     protected <T> T getFromRecents(Function<RecentsActivity, T> f) {
         if (!TestHelpers.isInLauncherProcess()) return null;
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.GET_RECENTS_FAILED, "getFromRecents");
+        }
         Object[] result = new Object[1];
         Wait.atMost("Failed to get from recents", () -> MAIN_EXECUTOR.submit(() -> {
             RecentsActivity activity = RecentsActivity.ACTIVITY_TRACKER.getCreatedActivity();
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "activity=" + activity);
+            }
             if (activity == null) {
                 return false;
             }
@@ -200,8 +208,13 @@
                 () -> mLauncher.getRecentTasks().size() >= 3, DEFAULT_ACTIVITY_TIMEOUT, mLauncher);
 
         BaseOverview overview = mLauncher.getBackground().switchToOverview();
-        executeOnRecents(recents ->
-                assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3));
+        executeOnRecents(recents -> {
+            if (TestProtocol.sDebugTracing) {
+                Log.d(TestProtocol.GET_RECENTS_FAILED, "isLoading=" +
+                        recents.<RecentsView>getOverviewPanel().isLoadingTasks());
+            }
+            assertTrue("Don't have at least 3 tasks", getTaskCount(recents) >= 3);
+        });
 
         // Test flinging forward and backward.
         overview.flingForward();
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
index 35383d2..67840d1 100644
--- a/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
+++ b/quickstep/tests/src/com/android/quickstep/NavigationModeSwitchRule.java
@@ -200,8 +200,8 @@
                 () -> launcher.getNavigationModel() == expectedMode, WAIT_TIME_MS, launcher);
 
         Wait.atMost(() -> "Switching nav mode: "
-                        + launcher.getNavigationModeMismatchError(),
-                () -> launcher.getNavigationModeMismatchError() == null,
+                        + launcher.getNavigationModeMismatchError(false),
+                () -> launcher.getNavigationModeMismatchError(false) == null,
                 WAIT_TIME_MS, launcher);
         AbstractLauncherUiTest.checkDetectedLeaks(launcher);
         return true;
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
index 72116eb..4ca1f59 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
@@ -49,7 +49,6 @@
 import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.Until;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Background;
@@ -58,6 +57,7 @@
 import com.android.launcher3.testcomponent.TestCommandReceiver;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.Before;
diff --git a/res/drawable/bg_widgets_searchbox.xml b/res/drawable/bg_widgets_searchbox.xml
new file mode 100644
index 0000000..81dd2aa
--- /dev/null
+++ b/res/drawable/bg_widgets_searchbox.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <solid android:color="#FFFFF7" />
+    <corners android:radius="24dp" />
+</shape>
\ No newline at end of file
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index 32a5419..1cdee08 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -21,4 +21,5 @@
     android:textColor="?attr/folderTextColor"
     android:includeFontPadding="false"
     android:hapticFeedbackEnabled="false"
-    launcher:iconDisplay="folder" />
+    launcher:iconDisplay="folder"
+    launcher:centerVertically="true" />
diff --git a/res/layout/live_preview_widget_cell.xml b/res/layout/live_preview_widget_cell.xml
index 7a42d19..1e1ce6e 100644
--- a/res/layout/live_preview_widget_cell.xml
+++ b/res/layout/live_preview_widget_cell.xml
@@ -15,14 +15,16 @@
 -->
 <com.android.launcher3.dragndrop.LivePreviewWidgetCell
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
+    android:layout_width="0dp"
     android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/widget_cell_horizontal_padding"
+    android:paddingVertical="@dimen/widget_cell_vertical_padding"
     android:layout_weight="1"
     android:orientation="vertical"
     android:focusable="true"
     android:background="?android:attr/colorPrimaryDark"
     android:gravity="center_horizontal">
 
-    <include layout="@layout/widget_cell_content"  />
+    <include layout="@layout/widget_cell_content" />
 
 </com.android.launcher3.dragndrop.LivePreviewWidgetCell>
\ No newline at end of file
diff --git a/res/layout/personal_work_tabs.xml b/res/layout/personal_work_tabs.xml
index 8f29997..5fb5bcb 100644
--- a/res/layout/personal_work_tabs.xml
+++ b/res/layout/personal_work_tabs.xml
@@ -23,6 +23,7 @@
     android:layout_marginLeft="@dimen/all_apps_tabs_side_padding"
     android:layout_marginRight="@dimen/all_apps_tabs_side_padding"
     android:orientation="horizontal"
+    android:elevation="2dp"
     style="@style/TextHeadline">
 
     <Button
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index c230dad..d38a77a 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -27,15 +27,12 @@
         android:clipToPadding="false"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp"
-        android:paddingTop="16dp"
         launcher:pageIndicator="@+id/folder_page_indicator" />
 
     <LinearLayout
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="48dp"
         android:clipChildren="false"
         android:orientation="horizontal"
         android:paddingLeft="12dp"
@@ -53,13 +50,10 @@
             android:gravity="center_horizontal"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
-            android:paddingBottom="@dimen/folder_label_padding_bottom"
-            android:paddingTop="@dimen/folder_label_padding_top"
             android:singleLine="true"
             android:textColor="?attr/folderTextColor"
             android:textColorHighlight="?android:attr/colorControlHighlight"
-            android:textColorHint="?attr/folderHintColor"
-            android:textSize="@dimen/folder_label_text_size" />
+            android:textColorHint="?attr/folderHintColor"/>
 
         <com.android.launcher3.pageindicators.PageIndicatorDots
             android:id="@+id/folder_page_indicator"
diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml
index 148a99b..73a5737 100644
--- a/res/layout/widget_cell.xml
+++ b/res/layout/widget_cell.xml
@@ -15,8 +15,10 @@
 -->
 <com.android.launcher3.widget.WidgetCell
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
+    android:layout_width="0dp"
     android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/widget_cell_horizontal_padding"
+    android:paddingVertical="@dimen/widget_cell_vertical_padding"
     android:layout_weight="1"
     android:orientation="vertical"
     android:focusable="true"
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 64f2362..65a49ab 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -17,47 +17,47 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content">
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingTop="@dimen/widget_preview_label_vertical_padding"
-        android:paddingBottom="@dimen/widget_preview_label_vertical_padding"
-        android:paddingLeft="@dimen/widget_preview_label_horizontal_padding"
-        android:paddingRight="@dimen/widget_preview_label_horizontal_padding"
-        android:orientation="horizontal">
-
-        <!-- The name of the widget. -->
-        <TextView
-            android:id="@+id/widget_name"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:ellipsize="end"
-            android:fadingEdge="horizontal"
-            android:gravity="start"
-            android:singleLine="true"
-            android:maxLines="1"
-            android:textColor="?android:attr/textColorSecondary"
-            android:textSize="14sp" />
-
-        <!-- The original dimensions of the widget (can't be the same text as above due to different
-             style. -->
-        <TextView
-            android:id="@+id/widget_dims"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="5dp"
-            android:layout_marginLeft="5dp"
-            android:textColor="?android:attr/textColorSecondary"
-            android:textSize="14sp"
-            android:alpha="0.8" />
-    </LinearLayout>
-
     <!-- The image of the widget. This view does not support padding. Any placement adjustment
          should be done using margins. -->
     <com.android.launcher3.widget.WidgetImageView
         android:id="@+id/widget_preview"
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="0dp"
-        android:layout_weight="1" />
+        android:layout_weight="1"
+        android:layout_marginVertical="8dp" />
+
+    <!-- The name of the widget. -->
+    <TextView
+        android:id="@+id/widget_name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:fadingEdge="horizontal"
+        android:gravity="center_horizontal"
+        android:singleLine="true"
+        android:maxLines="1"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textSize="14sp" />
+
+    <!-- The original dimensions of the widget (can't be the same text as above due to different
+         style. -->
+    <TextView
+        android:id="@+id/widget_dims"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textColor="?android:attr/textColorSecondary"
+        android:textSize="14sp"
+        android:alpha="0.8" />
+
+    <TextView
+        android:id="@+id/widget_description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textSize="12sp"
+        android:maxLines="2"
+        android:ellipsize="end"
+        android:fadingEdge="horizontal" />
+
 </merge>
\ No newline at end of file
diff --git a/res/layout/widgets_bottom_sheet.xml b/res/layout/widgets_bottom_sheet.xml
index 3fdfc96..c1b2cbf 100644
--- a/res/layout/widgets_bottom_sheet.xml
+++ b/res/layout/widgets_bottom_sheet.xml
@@ -44,11 +44,16 @@
         android:textSize="14sp"
         android:text="@string/long_press_widget_to_add"/>
 
-    <include layout="@layout/widgets_scroll_container"
-         android:id="@+id/widgets"
-         android:layout_width="wrap_content"
-         android:layout_height="wrap_content"
-         android:layout_marginTop="45dp"
-         android:layout_marginBottom="40dp"/>
+    <ScrollView
+        android:id="@+id/widgets_table_scroll_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="45dp"
+        android:layout_marginBottom="40dp">
+        <include layout="@layout/widgets_table_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal" />
+    </ScrollView>
 
 </com.android.launcher3.widget.WidgetsBottomSheet>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index cfbb6dd..8125db8 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -16,13 +16,15 @@
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto">
 
-    <include layout="@layout/personal_work_tabs" />
+    <include layout="@layout/personal_work_tabs"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="16dp" />
 
     <com.android.launcher3.workprofile.PersonalWorkPagedView
         android:id="@+id/widgets_view_pager"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_below="@+id/tabs"
         android:clipToPadding="false"
         android:descendantFocusability="afterDescendants"
         launcher:pageIndicator="@+id/tabs">
diff --git a/res/layout/widgets_full_sheet_search_and_recommendations.xml b/res/layout/widgets_full_sheet_search_and_recommendations.xml
new file mode 100644
index 0000000..9a6f922
--- /dev/null
+++ b/res/layout/widgets_full_sheet_search_and_recommendations.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/search_and_recommendations_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="16dp"
+    android:orientation="vertical">
+    <View
+        android:id="@+id/collapse_handle"
+        android:layout_width="48dp"
+        android:layout_height="2dp"
+        android:layout_gravity="center_horizontal"
+        android:background="@color/popup_color_primary_dark"/>
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:textSize="24sp"
+        android:layout_marginTop="16dp"
+        android:text="@string/widget_button_text"/>
+    <!-- Disable the search bar because it has not been implemented. -->
+    <EditText
+        android:id="@+id/widgets_search_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:layout_marginTop="16dp"
+        android:background="@drawable/bg_widgets_searchbox"
+        android:drawablePadding="8dp"
+        android:drawableStart="@drawable/ic_allapps_search"
+        android:hint="@string/widgets_full_sheet_search_bar_hint"
+        android:padding="12dp" />
+</LinearLayout>
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index faff10c..041e007 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -52,7 +52,7 @@
             android:id="@+id/app_subtitle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            tools:text="n widgets" />
+            tools:text="m widgets, n shortcuts" />
 
     </LinearLayout>
 
diff --git a/res/layout/widgets_scroll_container.xml b/res/layout/widgets_scroll_container.xml
deleted file mode 100644
index fc509d1..0000000
--- a/res/layout/widgets_scroll_container.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-
-<HorizontalScrollView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/widgets_scroll_container"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:background="?android:attr/colorPrimaryDark"
-    android:scrollbars="none">
-    <LinearLayout
-        android:id="@+id/widgets_cell_list"
-        style="@style/TextTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingStart="0dp"
-        android:paddingEnd="0dp"
-        android:orientation="horizontal"
-        android:showDividers="none"/>
-</HorizontalScrollView>
\ No newline at end of file
diff --git a/res/layout/widgets_table_container.xml b/res/layout/widgets_table_container.xml
index ffa239a..c4dfe7e 100644
--- a/res/layout/widgets_table_container.xml
+++ b/res/layout/widgets_table_container.xml
@@ -18,4 +18,5 @@
     android:id="@+id/widgets_table"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:layout_marginHorizontal="8dp"
     android:background="?android:attr/colorPrimaryDark" />
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 587df6d..4078ef4 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -133,6 +133,8 @@
         <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
+        <attr name="isScalable" format="boolean" />
+
     </declare-styleable>
 
     <declare-styleable name="DevicePadding">
@@ -150,6 +152,12 @@
         <attr name="minWidthDps" format="float" />
         <attr name="minHeightDps" format="float" />
 
+        <!-- These min cell values are only used if GridDisplayOption#isScalable is true-->
+        <attr name="minCellHeightDps" format="float" />
+        <attr name="minCellWidthDps" format="float" />
+
+        <attr name="borderSpacingDps" format="float" />
+
         <attr name="iconImageSize" format="float" />
         <!-- landscapeIconSize defaults to iconSize, if not specified -->
         <attr name="landscapeIconSize" format="float" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 41d1a12..89415b8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,7 +24,7 @@
 
     <!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
-    <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
+    <integer name="config_workspaceSpringLoadShrinkPercentage">85</integer>
 
     <!-- The duration of the animation from search hint to text entry -->
     <integer name="config_searchHintAnimationDuration">50</integer>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index acc6466..3b171c6 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,6 +20,7 @@
 
     <!-- Dynamic Grid -->
     <dimen name="dynamic_grid_edge_margin">8dp</dimen>
+    <dimen name="dynamic_grid_left_right_margin">8dp</dimen>
     <dimen name="dynamic_grid_icon_drawable_padding">7dp</dimen>
     <!-- Minimum space between workspace and hotseat in spring loaded mode -->
     <dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
@@ -27,7 +28,6 @@
     <dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
     <dimen name="dynamic_grid_cell_layout_padding">5.5dp</dimen>
     <dimen name="dynamic_grid_cell_padding_x">8dp</dimen>
-    <dimen name="dynamic_grid_cell_padding_y">7dp</dimen>
 
     <!-- Hotseat -->
     <dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
@@ -37,6 +37,9 @@
     <dimen name="dynamic_grid_hotseat_extra_vertical_size">34dp</dimen>
     <dimen name="dynamic_grid_hotseat_side_padding">0dp</dimen>
 
+    <!-- Scalable Grid -->
+    <dimen name="scalable_grid_left_right_margin">22dp</dimen>
+
     <!-- Workspace page indicator -->
     <dimen name="workspace_page_indicator_height">24dp</dimen>
     <dimen name="workspace_page_indicator_line_height">1dp</dimen>
@@ -90,7 +93,6 @@
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
 
-
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
     <dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
@@ -102,8 +104,8 @@
     <dimen name="work_profile_footer_text_size">16sp</dimen>
 
 <!-- Widget tray -->
-    <dimen name="widget_preview_label_vertical_padding">8dp</dimen>
-    <dimen name="widget_preview_label_horizontal_padding">16dp</dimen>
+    <dimen name="widget_cell_vertical_padding">8dp</dimen>
+    <dimen name="widget_cell_horizontal_padding">16dp</dimen>
 
     <dimen name="widget_preview_shadow_blur">0.5dp</dimen>
     <dimen name="widget_preview_key_shadow_distance">1dp</dimen>
@@ -148,10 +150,12 @@
 
     <dimen name="folder_cell_x_padding">9dp</dimen>
     <dimen name="folder_cell_y_padding">6dp</dimen>
-    <dimen name="folder_child_text_size">13sp</dimen>
-    <dimen name="folder_label_padding_top">12dp</dimen>
-    <dimen name="folder_label_padding_bottom">12dp</dimen>
-    <dimen name="folder_label_text_size">16sp</dimen>
+    <!-- label text size = workspace text size multiplied by this scale -->
+    <dimen name="folder_label_text_scale">1.14</dimen>
+    <dimen name="folder_label_height">48dp</dimen>
+
+    <dimen name="folder_content_padding_left_right">8dp</dimen>
+    <dimen name="folder_content_padding_top">16dp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
@@ -183,13 +187,11 @@
     <dimen name="popup_padding_start">10dp</dimen>
     <dimen name="popup_padding_end">16dp</dimen>
     <dimen name="popup_vertical_padding">4dp</dimen>
-    <dimen name="popup_arrow_width">10dp</dimen>
-    <dimen name="popup_arrow_height">8dp</dimen>
-    <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
+    <dimen name="popup_arrow_width">12dp</dimen>
+    <dimen name="popup_arrow_height">10dp</dimen>
+    <dimen name="popup_arrow_vertical_offset">-1dp</dimen>
     <!-- popup_padding_start + deep_shortcut_icon_size / 2 -->
-    <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
-    <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
-    <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
+    <dimen name="popup_arrow_horizontal_center_offset">28dp</dimen>
     <dimen name="popup_arrow_corner_radius">2dp</dimen>
     <!-- popup_padding_start + icon_size + 10dp -->
     <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5a9def7..44b5ee7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -52,11 +52,28 @@
     <string name="add_item_request_drag_hint">Touch &amp; hold to place manually</string>
     <!-- Button label to automatically add icon on home screen [CHAR_LIMIT=50] -->
     <string name="place_automatically">Add automatically</string>
-    <!-- Label for showing the number of widgets an app has in the full widgets picker. [CHAR_LIMIT=25] -->
-    <plurals name="widgets_tray_subtitle">
-        <item quantity="one"><xliff:g id="widget_count" example="1">%1$d</xliff:g> widget</item>
-        <item quantity="other"><xliff:g id="widget_count" example="2">%1$d</xliff:g> widgets</item>
+    <!-- Label for showing the number of widgets an app has in the full widgets picker.
+         [CHAR_LIMIT=25] -->
+    <plurals name="widgets_count">
+        <item quantity="one"><xliff:g id="widgets_count" example="1">%1$d</xliff:g> widget</item>
+        <item quantity="other"><xliff:g id="widgets_count" example="2">%1$d</xliff:g> widgets</item>
     </plurals>
+    <!-- Label for showing the number of shortcut an app has in the full widgets picker.
+         [CHAR_LIMIT=25] -->
+    <plurals name="shortcuts_count">
+        <item quantity="one"><xliff:g id="shortcuts_count" example="1">%1$d</xliff:g> shortcut</item>
+        <item quantity="other"><xliff:g id="shortcuts_count" example="2">%1$d</xliff:g> shortcuts</item>
+    </plurals>
+    <!-- Label for showing both the number of widgets and shortcuts an app has in the full widgets
+         picker. [CHAR_LIMIT=50] -->
+    <string name="widgets_and_shortcuts_count"><xliff:g id="widgets_count" example="5 widgets">%1$s</xliff:g>, <xliff:g id="shortcuts_count" example="1 shortcut">%2$s</xliff:g></string>
+
+    <!-- Text for both the tile of a popup view, which shows all available widgets installed on
+         the device, and the text of a button, which opens this popup view. [CHAR LIMIT=30]-->
+    <string name="widget_button_text">Widgets</string>
+    <!-- Search bar text shown in the popup view showing all available widgets installed on the
+         device. [CHAR_LIMIT=50] -->
+    <string name="widgets_full_sheet_search_bar_hint">Search</string>
 
     <!-- All Apps -->
     <!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
@@ -185,8 +202,6 @@
     <string name="folder_name_format_overflow">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g>, <xliff:g id="size" example="2">%2$d</xliff:g> or more items</string>
 
     <!-- Strings for the customization mode -->
-    <!-- Text for widget add button [CHAR LIMIT=30]-->
-    <string name="widget_button_text">Widgets</string>
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
     <string name="wallpaper_button_text">Wallpapers</string>
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
diff --git a/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
new file mode 100644
index 0000000..92f77f2
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfoTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.widget;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Point;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public final class LauncherAppWidgetProviderInfoTest {
+
+    private static final int CELL_SIZE = 50;
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void initSpans_minWidthSmallerThanCellWidth_shouldInitializeSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minWidth = 20;
+        info.minHeight = 20;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.spanX).isEqualTo(1);
+        assertThat(info.spanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minWidthLargerThanCellWidth_shouldInitializeSpans() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minWidth = 80;
+        info.minHeight = 80;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.spanX).isEqualTo(2);
+        assertThat(info.spanY).isEqualTo(2);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthUnspecified_shouldInitializeMinSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(1);
+        assertThat(info.minSpanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthSmallerThanCellWidth_shouldInitializeMinSpansToOne() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minResizeWidth = 20;
+        info.minResizeHeight = 20;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(1);
+        assertThat(info.minSpanY).isEqualTo(1);
+    }
+
+    @Test
+    public void initSpans_minResizeWidthLargerThanCellWidth_shouldInitializeMinSpans() {
+        LauncherAppWidgetProviderInfo info = new LauncherAppWidgetProviderInfo();
+        info.minResizeWidth = 80;
+        info.minResizeHeight = 80;
+        InvariantDeviceProfile idp = createIDP();
+
+        info.initSpans(mContext, idp);
+
+        assertThat(info.minSpanX).isEqualTo(2);
+        assertThat(info.minSpanY).isEqualTo(2);
+    }
+
+    private InvariantDeviceProfile createIDP() {
+        DeviceProfile profile = Mockito.mock(DeviceProfile.class);
+        Mockito.when(profile.getCellSize()).thenReturn(new Point(CELL_SIZE, CELL_SIZE));
+
+        InvariantDeviceProfile idp = new InvariantDeviceProfile();
+        idp.landscapeProfile = idp.portraitProfile = profile;
+        return idp;
+    }
+
+}
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
index 04797a6..b972c6f 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java
@@ -32,12 +32,12 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
index e94b253..a7c8d92 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java
@@ -31,13 +31,13 @@
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index ae5b9a5..848630e 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -34,7 +34,6 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.ComponentWithLabel;
@@ -42,6 +41,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
 import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 358e6e0..8a0cf34 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -37,7 +37,6 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.icons.BitmapInfo;
@@ -46,6 +45,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
 import com.android.launcher3.testing.TestActivity;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
index 86df3f8..2d22c45 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -26,11 +26,11 @@
 import android.content.Context;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
 
 import org.junit.Before;
diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
index 5922223..e68edd3 100644
--- a/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
+++ b/robolectric_tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -30,11 +30,11 @@
 import android.os.UserHandle;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.icons.ComponentWithLabel;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import org.junit.Before;
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index be92c5d..3b28d4d 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -3,7 +3,6 @@
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
 import static com.android.launcher3.Utilities.ATLEAST_S;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
 
@@ -13,12 +12,11 @@
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
-import android.content.res.Configuration;
 import android.graphics.Point;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.AttributeSet;
+import android.util.SizeF;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
@@ -31,6 +29,7 @@
 import com.android.launcher3.util.FocusLogic;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -50,6 +49,14 @@
                         inv.portraitProfile.getCellSize()};
             });
 
+    // Represents the border spacing size on the grid in the two orientations.
+    public static final MainThreadInitializedObject<int[]> BORDER_SPACING_SIZE =
+            new MainThreadInitializedObject<>(c -> {
+                InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
+                return new int[] {inv.landscapeProfile.cellLayoutBorderSpacingPx,
+                        inv.portraitProfile.cellLayoutBorderSpacingPx};
+            });
+
     private static final int HANDLE_COUNT = 4;
     private static final int INDEX_LEFT = 0;
     private static final int INDEX_TOP = 1;
@@ -360,7 +367,7 @@
 
     public static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
                                               int spanX, int spanY) {
-        List<PointF> sizes = getWidgetSizes(launcher, spanX, spanY);
+        List<SizeF> sizes = getWidgetSizes(launcher, spanX, spanY);
         if (ATLEAST_S) {
             widgetView.updateAppWidgetSize(new Bundle(), sizes);
         } else {
@@ -370,45 +377,25 @@
         }
     }
 
-    private static PointF getWidgetSize(Context context, Point cellSize, int spanX, int spanY) {
+    private static SizeF getWidgetSize(Context context, Point cellSize, int spanX, int spanY,
+            int borderSpacing) {
         final float density = context.getResources().getDisplayMetrics().density;
-        float hBorderSpacing = 0;
-        float vBorderSpacing = 0;
-        if (ENABLE_FOUR_COLUMNS.get()) {
-            final int borderSpacing = context.getResources()
-                    .getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
-            hBorderSpacing = (spanX - 1) * borderSpacing;
-            vBorderSpacing = (spanY - 1) * borderSpacing;
-        }
-        PointF widgetSize = new PointF();
-        widgetSize.x = ((spanX * cellSize.x) + hBorderSpacing) / density;
-        widgetSize.y = ((spanY * cellSize.y) + vBorderSpacing) / density;
-        return widgetSize;
-    }
+        final float hBorderSpacing = (spanX - 1) * borderSpacing;
+        final float vBorderSpacing = (spanY - 1) * borderSpacing;
 
-    /** Returns the actual widget size given its span. */
-    public static PointF getWidgetSize(Context context, int spanX, int spanY) {
-        final Point[] cellSize = CELL_SIZE.get(context);
-        if (isLandscape(context)) {
-            return getWidgetSize(context, cellSize[0], spanX, spanY);
-        }
-        return getWidgetSize(context, cellSize[1], spanX, spanY);
-    }
-
-    /** Returns true if the screen is in landscape mode. */
-    private static boolean isLandscape(Context context) {
-        return context.getResources().getConfiguration().orientation
-                == Configuration.ORIENTATION_LANDSCAPE;
+        return new SizeF(((spanX * cellSize.x) + hBorderSpacing) / density,
+                ((spanY * cellSize.y) + vBorderSpacing) / density);
     }
 
     /** Returns the list of sizes for a widget of given span, in dp. */
-    public static ArrayList<PointF> getWidgetSizes(Context context, int spanX, int spanY) {
+    public static ArrayList<SizeF> getWidgetSizes(Context context, int spanX, int spanY) {
         final Point[] cellSize = CELL_SIZE.get(context);
+        final int[] borderSpacing = BORDER_SPACING_SIZE.get(context);
 
-        PointF landSize = getWidgetSize(context, cellSize[0], spanX, spanY);
-        PointF portSize = getWidgetSize(context, cellSize[1], spanX, spanY);
+        SizeF landSize = getWidgetSize(context, cellSize[0], spanX, spanY, borderSpacing[0]);
+        SizeF portSize = getWidgetSize(context, cellSize[1], spanX, spanY, borderSpacing[1]);
 
-        ArrayList<PointF> sizes = new ArrayList<>(2);
+        ArrayList<SizeF> sizes = new ArrayList<>(2);
         sizes.add(landSize);
         sizes.add(portSize);
         return sizes;
@@ -424,17 +411,18 @@
      * the right (resp. bottom) for the max. The returned rectangle is set with 0s if the list is
      * empty.
      */
-    public static Rect getMinMaxSizes(List<PointF> sizes, @Nullable Rect outRect) {
+    public static Rect getMinMaxSizes(List<SizeF> sizes, @Nullable Rect outRect) {
         if (outRect == null) {
             outRect = new Rect();
         }
         if (sizes.isEmpty()) {
             outRect.set(0, 0, 0, 0);
         } else {
-            PointF first = sizes.get(0);
-            outRect.set((int) first.x, (int) first.y, (int) first.x, (int) first.y);
+            SizeF first = sizes.get(0);
+            outRect.set((int) first.getWidth(), (int) first.getHeight(), (int) first.getWidth(),
+                    (int) first.getHeight());
             for (int i = 1; i < sizes.size(); i++) {
-                outRect.union((int) sizes.get(i).x, (int) sizes.get(i).y);
+                outRect.union((int) sizes.get(i).getWidth(), (int) sizes.get(i).getHeight());
             }
         }
         return outRect;
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index b85c648..75e89b2 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -21,6 +21,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
 
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index c55b46b..9369bdc 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -23,6 +23,7 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityNodeInfo;
 
+import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -188,4 +189,21 @@
         super.onInitializeAccessibilityNodeInfo(info);
         if (isLayoutSuppressed()) info.setScrollable(false);
     }
+
+    /**
+     * Scrolls this recycler view to the top.
+     */
+    public void scrollToTop() {
+        if (mScrollbar != null) {
+            mScrollbar.reattachThumbToScroll();
+        }
+        if (getLayoutManager() instanceof LinearLayoutManager) {
+            LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
+                // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
+                return;
+            }
+        }
+        scrollToPosition(0);
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ee4d7ec..e0be6de 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -17,7 +17,6 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.FastBitmapDrawable.newIcon;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -194,7 +193,7 @@
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
-            setCenterVertically(ENABLE_FOUR_COLUMNS.get());
+            setCenterVertically(grid.isScalableGrid);
         } else if (mDisplay == DISPLAY_ALL_APPS) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 947388b..b8833cf 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -53,6 +53,7 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
 import androidx.core.view.ViewCompat;
 
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -2663,20 +2664,21 @@
         }
 
         public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
-                int rowCount, int borderSpacing) {
+                int rowCount, int borderSpacing, @Nullable Rect inset) {
             setup(cellWidth, cellHeight, invertHorizontally, colCount, rowCount, 1.0f, 1.0f,
-                    borderSpacing);
+                    borderSpacing, inset);
         }
 
         /**
-         * Use this method, as opposed to {@link #setup(int, int, boolean, int, int, int)},
+         * Use this method, as opposed to {@link #setup(int, int, boolean, int, int, int, Rect)},
          * if the view needs to be scaled.
          *
          * ie. In multi-window mode, we setup widgets so that they are measured and laid out
          * using their full/invariant device profile sizes.
          */
         public void setup(int cellWidth, int cellHeight, boolean invertHorizontally, int colCount,
-                int rowCount, float cellScaleX, float cellScaleY, int borderSpacing) {
+                int rowCount, float cellScaleX, float cellScaleY, int borderSpacing,
+                @Nullable Rect inset) {
             if (isLockedToGrid) {
                 final int myCellHSpan = cellHSpan;
                 final int myCellVSpan = cellVSpan;
@@ -2697,6 +2699,13 @@
                 height = Math.round(myCellHeight) - topMargin - bottomMargin;
                 x = leftMargin + (myCellX * cellWidth) + (myCellX * borderSpacing);
                 y = topMargin + (myCellY * cellHeight) + (myCellY * borderSpacing);
+
+                if (inset != null) {
+                    x -= inset.left;
+                    y -= inset.top;
+                    width += inset.left + inset.right;
+                    height += inset.top + inset.bottom;
+                }
             }
         }
 
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 634093c..f2dd60e 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
+import static com.android.launcher3.ResourceUtils.pxFromDp;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -66,6 +66,8 @@
 
     public final float aspectRatio;
 
+    public final boolean isScalableGrid;
+
     /**
      * The maximum amount of left/right workspace padding as a percentage of the screen width.
      * To be clear, this means that up to 7% of the screen width can be used as left padding, and
@@ -79,8 +81,10 @@
     private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4;
 
     // Workspace
-    public final int desiredWorkspaceLeftRightMarginPx;
-    public final int cellLayoutBorderSpacingPx;
+    public final int desiredWorkspaceLeftRightOriginalPx;
+    public int desiredWorkspaceLeftRightMarginPx;
+    public final int cellLayoutBorderSpacingOriginalPx;
+    public int cellLayoutBorderSpacingPx;
     public final int cellLayoutPaddingLeftRightPx;
     public final int cellLayoutBottomPaddingPx;
     public final int edgeMarginPx;
@@ -102,13 +106,21 @@
 
     public int cellWidthPx;
     public int cellHeightPx;
-    public int cellYPaddingPx;
     public int workspaceCellPaddingXPx;
 
+    public int cellYPaddingPx;
+    public int cellYPaddingOriginalPx;
+
     // Folder
+    public float folderLabelTextScale;
+    public int folderLabelTextSizePx;
     public int folderIconSizePx;
     public int folderIconOffsetYPx;
 
+    // Folder content
+    public int folderContentPaddingLeftRight;
+    public int folderContentPaddingTop;
+
     // Folder cell
     public int folderCellWidthPx;
     public int folderCellHeightPx;
@@ -164,9 +176,12 @@
         this.inv = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
+        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
         windowX = windowPosition.x;
         windowY = windowPosition.y;
 
+        isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
+
         // Determine sizes.
         widthPx = width;
         heightPx = height;
@@ -193,8 +208,6 @@
         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
 
         // Some more constants
-        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
-
         context = getContext(context, info, isVerticalBarLayout()
                 ? Configuration.ORIENTATION_LANDSCAPE
                 : Configuration.ORIENTATION_PORTRAIT);
@@ -217,16 +230,25 @@
         availableHeightPx = nonFinalAvailableHeightPx;
 
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
-        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
 
-        cellYPaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_y);
-        cellLayoutBorderSpacingPx = isVerticalBarLayout()
-                || isMultiWindowMode
-                || !ENABLE_FOUR_COLUMNS.get()
-                ? 0 : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
+        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : isScalableGrid
+                ? res.getDimensionPixelSize(R.dimen.scalable_grid_left_right_margin)
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_left_right_margin);
+        desiredWorkspaceLeftRightOriginalPx = desiredWorkspaceLeftRightMarginPx;
+
+        folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
+        folderContentPaddingLeftRight =
+                res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+        folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+        setCellLayoutBorderSpacing(pxFromDp(inv.borderSpacing, mInfo.metrics, 1f));
+        cellLayoutBorderSpacingOriginalPx = cellLayoutBorderSpacingPx;
+
         int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
                 ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
-        int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
+        int cellLayoutPadding = isScalableGrid
+                ? 0
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
         if (isLandscape) {
             cellLayoutPaddingLeftRightPx = 0;
             cellLayoutBottomPaddingPx = cellLayoutPadding;
@@ -259,16 +281,16 @@
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
         int hotseatExtraVerticalSize =
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+        hotseatBarSizePx = pxFromDp(inv.iconSize, mInfo.metrics, 1f)
                 + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
                 : (hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx
-                        + (ENABLE_FOUR_COLUMNS.get() ? 0 : hotseatExtraVerticalSize)));
+                        + (isScalableGrid ? 0 : hotseatExtraVerticalSize)));
 
         // Calculate all of the remaining variables.
         int extraSpace = updateAvailableDimensions(res);
         // Now that we have all of the variables calculated, we can tune certain sizes.
-        if (ENABLE_FOUR_COLUMNS.get()) {
+        if (isScalableGrid) {
             DevicePadding padding = inv.devicePaddings.getDevicePadding(extraSpace);
             workspaceTopPadding = padding.getWorkspaceTopPadding(extraSpace);
             workspaceBottomPadding = padding.getWorkspaceBottomPadding(extraSpace);
@@ -299,6 +321,30 @@
                         IconShape.DEFAULT_PATH_SIZE);
     }
 
+    private void setCellLayoutBorderSpacing(int borderSpacing) {
+        if (isScalableGrid) {
+            cellLayoutBorderSpacingPx = borderSpacing;
+            folderContentPaddingLeftRight = borderSpacing;
+            folderContentPaddingTop = borderSpacing;
+        } else {
+            cellLayoutBorderSpacingPx = 0;
+        }
+    }
+
+    /**
+     * We inset the widget padding added by the system and instead rely on the border spacing
+     * between cells to create reliable consistency between widgets
+     */
+    public boolean shouldInsetWidgets() {
+        Rect widgetPadding = inv.defaultWidgetPadding;
+
+        // Check all sides to ensure that the widget won't overlap into another cell.
+        return cellLayoutBorderSpacingPx > widgetPadding.left
+                && cellLayoutBorderSpacingPx > widgetPadding.top
+                && cellLayoutBorderSpacingPx > widgetPadding.right
+                && cellLayoutBorderSpacingPx > widgetPadding.bottom;
+    }
+
     public Builder toBuilder(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
         return new Builder(context, inv, mInfo)
@@ -380,20 +426,42 @@
     private int updateAvailableDimensions(Resources res) {
         updateIconSize(1f, res);
 
-        // Check to see if the icons fit within the available height.  If not, then scale down.
-        float usedHeight = (cellHeightPx * inv.numRows)
-                + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
-        int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
+        Point workspacePadding = getTotalWorkspacePadding();
+
+        // Check to see if the icons fit within the available height.
+        float usedHeight = getCellLayoutHeight();
+        final int maxHeight = availableHeightPx - workspacePadding.y;
         float extraHeight = Math.max(0, maxHeight - usedHeight);
-        if (usedHeight > maxHeight) {
-            float scale = maxHeight / usedHeight;
-            updateIconSize(scale, res);
-            extraHeight = 0;
+        float scaleY = maxHeight / usedHeight;
+        boolean shouldScale = scaleY < 1f;
+
+        float scaleX = 1f;
+        if (isScalableGrid) {
+            // We scale to fit the cellWidth and cellHeight in the available space.
+            // The benefit of scalable grids is that we can get consistent aspect ratios between
+            // devices.
+            float usedWidth = (cellWidthPx * inv.numColumns)
+                    + (cellLayoutBorderSpacingPx * (inv.numColumns - 1))
+                    + (desiredWorkspaceLeftRightMarginPx * 2);
+            // We do not subtract padding here, as we also scale the workspace padding if needed.
+            scaleX = availableWidthPx / usedWidth;
+            shouldScale = true;
         }
+
+        if (shouldScale) {
+            float scale = Math.min(scaleX, scaleY);
+            updateIconSize(scale, res);
+            extraHeight = Math.max(0, maxHeight - getCellLayoutHeight());
+        }
+
         updateAvailableFolderCellDimensions(res);
         return Math.round(extraHeight);
     }
 
+    private int getCellLayoutHeight() {
+        return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
+    }
+
     /**
      * Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
@@ -403,17 +471,21 @@
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
-        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
-                * scale));
-        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
+        iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        iconTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
-        if (ENABLE_FOUR_COLUMNS.get()) {
-            cellHeightPx = iconSizePx + iconDrawablePaddingPx
-                    + Utilities.calculateTextHeight(iconTextSizePx)
-                    + (cellYPaddingPx * 2);
+        setCellLayoutBorderSpacing((int) (cellLayoutBorderSpacingOriginalPx * scale));
+
+        if (isScalableGrid) {
+            cellWidthPx = pxFromDp(inv.minCellWidth, mInfo.metrics, scale);
+            cellHeightPx = pxFromDp(inv.minCellHeight, mInfo.metrics, scale);
+            int cellContentHeight = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx);
+            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+            desiredWorkspaceLeftRightMarginPx = (int) (desiredWorkspaceLeftRightOriginalPx * scale);
         } else {
-            cellYPaddingPx = 0;
+            cellWidthPx = iconSizePx + iconDrawablePaddingPx;
             cellHeightPx = iconSizePx + iconDrawablePaddingPx
                     + Utilities.calculateTextHeight(iconTextSizePx);
             int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
@@ -426,11 +498,10 @@
                 iconDrawablePaddingPx = cellPaddingY;
             }
         }
-        cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
         // All apps
         if (allAppsHasDifferentNumColumns()) {
-            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
+            allAppsIconSizePx = pxFromDp(inv.allAppsIconSize, mInfo.metrics);
             allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
             allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
             // We use 4 below to ensure labels are closer to their corresponding icon.
@@ -474,12 +545,10 @@
     }
 
     private void updateAvailableFolderCellDimensions(Resources res) {
-        int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
-                + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
-                + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
-
         updateFolderCellSize(1f, res);
 
+        final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
+
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
         Point totalWorkspacePadding = getTotalWorkspacePadding();
@@ -488,13 +557,14 @@
         float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
                 + ((inv.numFolderRows - 1) * cellLayoutBorderSpacingPx);
         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
-                - folderMargin;
+                - folderMargin - folderContentPaddingTop;
         float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
         float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
                 + ((inv.numFolderColumns - 1) * cellLayoutBorderSpacingPx);
-        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
+        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
+                - folderContentPaddingLeftRight * 2;
         float scaleX = contentMaxWidth / contentUsedWidth;
 
         float scale = Math.min(scaleX, scaleY);
@@ -504,9 +574,10 @@
     }
 
     private void updateFolderCellSize(float scale, Resources res) {
-        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
-        folderChildTextSizePx =
-                (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
+        float invIconSizeDp = isVerticalBarLayout() ? inv.landscapeIconSize : inv.iconSize;
+        folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        folderChildTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
+        folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
 
         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
         int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale);
@@ -570,7 +641,8 @@
                 padding.right = hotseatBarSizePx;
             }
         } else {
-            int paddingBottom = hotseatBarSizePx + workspacePageIndicatorHeight
+            int hotseatTop = isTaskbarPresent ? taskbarSize : hotseatBarSizePx;
+            int paddingBottom = hotseatTop + workspacePageIndicatorHeight
                     + workspaceBottomPadding - mWorkspacePageIndicatorOverlapWorkspace;
             if (isTablet) {
                 // Pad the left and right of the workspace to ensure consistent spacing
@@ -580,15 +652,16 @@
                         ((inv.numColumns - 1) * cellWidthPx)));
                 availablePaddingX = (int) Math.min(availablePaddingX,
                         widthPx * MAX_HORIZONTAL_PADDING_PERCENT);
+                int hotseatVerticalPadding = isTaskbarPresent ? 0
+                        : hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx;
                 int availablePaddingY = Math.max(0, heightPx - edgeMarginPx - paddingBottom
-                        - (2 * inv.numRows * cellHeightPx) - hotseatBarTopPaddingPx
-                        - hotseatBarBottomPaddingPx);
+                        - (2 * inv.numRows * cellHeightPx) - hotseatVerticalPadding);
                 padding.set(availablePaddingX / 2, edgeMarginPx + availablePaddingY / 2,
                         availablePaddingX / 2, paddingBottom + availablePaddingY / 2);
             } else {
                 // Pad the top and bottom of the workspace with search/hotseat bar sizes
                 padding.set(desiredWorkspaceLeftRightMarginPx,
-                        workspaceTopPadding + edgeMarginPx,
+                        workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx),
                         desiredWorkspaceLeftRightMarginPx,
                         paddingBottom);
             }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 4f4f2a7..b2112ad 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -21,6 +21,7 @@
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -98,14 +99,20 @@
         } else {
             lp.gravity = Gravity.BOTTOM;
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
-            lp.height = grid.hotseatBarSizePx + insets.bottom;
+            lp.height = grid.isTaskbarPresent
+                    ? grid.taskbarSize
+                    : grid.hotseatBarSizePx + insets.bottom;
         }
-        Rect padding = grid.getHotseatLayoutPadding();
-        int paddingBottom = padding.bottom;
-        if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !grid.isVerticalBarLayout()) {
-            paddingBottom -= grid.hotseatBarBottomPaddingPx;
+        if (!grid.isTaskbarPresent) {
+            // When taskbar is present, we set the padding separately to ensure a seamless visual
+            // handoff between taskbar and hotseat during drag and drop.
+            Rect padding = grid.getHotseatLayoutPadding();
+            int paddingBottom = padding.bottom;
+            if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !grid.isVerticalBarLayout()) {
+                paddingBottom -= grid.hotseatBarBottomPaddingPx;
+            }
+            setPadding(padding.left, padding.top, padding.right, paddingBottom);
         }
-        setPadding(padding.left, padding.top, padding.right, paddingBottom);
 
         setLayoutParams(lp);
         InsettableFrameLayout.dispatchInsets(this, insets);
@@ -160,4 +167,11 @@
     protected void showInlineQsb() {
         //Does nothing
     }
+
+    /**
+     * Returns the first View for which the given itemOperator returns true, or null.
+     */
+    public View getFirstItemMatch(Workspace.ItemOperator itemOperator) {
+        return mWorkspace.getFirstMatch(new CellLayout[] { this }, itemOperator);
+    }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 2a08c50..bb60557 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -111,6 +111,10 @@
     public float allAppsIconSize;
     public float allAppsIconTextSize;
 
+    public float minCellHeight;
+    public float minCellWidth;
+    public float borderSpacing;
+
     private SparseArray<TypedValue> mExtraAttrs;
 
     /**
@@ -123,6 +127,11 @@
      */
     public int numAllAppsColumns;
 
+    /**
+     * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
+     */
+    protected boolean isScalable;
+
     public String dbFile;
     public int defaultLayoutId;
     int demoModeLayoutId;
@@ -154,6 +163,10 @@
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
+        isScalable = p.isScalable;
+        minCellHeight = p.minCellHeight;
+        minCellWidth = p.minCellWidth;
+        borderSpacing = p.borderSpacing;
         dbFile = p.dbFile;
         allAppsIconSize = p.allAppsIconSize;
         allAppsIconTextSize = p.allAppsIconTextSize;
@@ -213,6 +226,9 @@
         result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
         result.allAppsIconSize = Math.min(
                 defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+        result.minCellHeight = defaultDisplayOption.minCellHeight;
+        result.minCellWidth = defaultDisplayOption.minCellWidth;
+        result.borderSpacing = defaultDisplayOption.borderSpacing;
 
         devicePaddings = new DevicePaddings(context);
         initGrid(context, myInfo, result);
@@ -259,6 +275,7 @@
         numFolderRows = closestProfile.numFolderRows;
         numFolderColumns = closestProfile.numFolderColumns;
         numAllAppsColumns = closestProfile.numAllAppsColumns;
+        isScalable = closestProfile.isScalable;
 
         mExtraAttrs = closestProfile.extraAttrs;
 
@@ -269,6 +286,10 @@
         iconTextSize = displayOption.iconTextSize;
         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
 
+        minCellHeight = displayOption.minCellHeight;
+        minCellWidth = displayOption.minCellWidth;
+        borderSpacing = displayOption.borderSpacing;
+
         if (Utilities.isGridOptionsEnabled(context)) {
             allAppsIconSize = displayOption.allAppsIconSize;
             allAppsIconTextSize = displayOption.allAppsIconTextSize;
@@ -589,6 +610,8 @@
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
+        private final boolean isScalable;
+
         private final SparseArray<TypedValue> extraAttrs;
 
         public GridOption(Context context, AttributeSet attrs) {
@@ -612,6 +635,9 @@
             numAllAppsColumns = a.getInt(
                     R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
+            isScalable = a.getBoolean(
+                    R.styleable.GridDisplayOption_isScalable, false);
+
             a.recycle();
 
             extraAttrs = Themes.createValueMap(context, attrs,
@@ -626,6 +652,10 @@
         private final float minHeightDps;
         private final boolean canBeDefault;
 
+        private float minCellHeight;
+        private float minCellWidth;
+        private float borderSpacing;
+
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
@@ -643,6 +673,10 @@
             canBeDefault = a.getBoolean(
                     R.styleable.ProfileDisplayOption_canBeDefault, false);
 
+            minCellHeight = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0);
+            minCellWidth = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0);
+            borderSpacing = a.getFloat(R.styleable.ProfileDisplayOption_borderSpacingDps, 0);
+
             iconSize = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0);
             landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize,
                     iconSize);
@@ -664,6 +698,9 @@
             minWidthDps = 0;
             minHeightDps = 0;
             canBeDefault = false;
+            minCellHeight = 0;
+            minCellWidth = 0;
+            borderSpacing = 0;
         }
 
         private DisplayOption multiply(float w) {
@@ -672,6 +709,9 @@
             allAppsIconSize *= w;
             iconTextSize *= w;
             allAppsIconTextSize *= w;
+            minCellHeight *= w;
+            minCellWidth *= w;
+            borderSpacing *= w;
             return this;
         }
 
@@ -681,6 +721,9 @@
             allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
             allAppsIconTextSize += p.allAppsIconTextSize;
+            minCellHeight += p.minCellHeight;
+            minCellWidth += p.minCellWidth;
+            borderSpacing += p.borderSpacing;
             return this;
         }
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index f7ff262..fa63885 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -121,6 +121,7 @@
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.folder.FolderGridOrganizer;
 import com.android.launcher3.folder.FolderIcon;
@@ -181,7 +182,9 @@
 import com.android.launcher3.views.FloatingSurfaceView;
 import com.android.launcher3.views.OptionsPopupView;
 import com.android.launcher3.views.ScrimView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
@@ -2861,6 +2864,10 @@
         return false;
     }
 
+    public DragOptions getDefaultWorkspaceDragOptions() {
+        return new DragOptions();
+    }
+
     private static class NonConfigInstance {
         public Configuration config;
         public Bitmap snapshot;
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index e89b9b0..699495c 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -615,7 +615,9 @@
                         + "\" bitmapIcon=" + info.bitmap.icon
                         + " componentName=" + info.componentName.getPackageName());
             }
+            writer.println();
         }
+        mModelDelegate.dump(prefix, fd, writer, args);
         mBgDataModel.dump(prefix, fd, writer, args);
     }
 
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index e151777..2ec5e90 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -71,6 +71,7 @@
 import com.android.launcher3.util.NoLocaleSQLiteHelper;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 import org.xmlpull.v1.XmlPullParser;
 
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index f9a1ded..21c40ef 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -56,8 +56,10 @@
     public static final int ALL_APPS_HEADER_EXTRA = 1 << 3; // e.g. app predictions
     public static final int ALL_APPS_CONTENT = 1 << 4;
     public static final int VERTICAL_SWIPE_INDICATOR = 1 << 5;
-    public static final int OVERVIEW_BUTTONS = 1 << 6;
+    public static final int OVERVIEW_ACTIONS = 1 << 6;
     public static final int TASKBAR = 1 << 7;
+    public static final int CLEAR_ALL_BUTTON = 1 << 8;
+    public static final int WORKSPACE_PAGE_INDICATOR = 1 << 9;
 
     /** Mask of all the items that are contained in the apps view. */
     public static final int APPS_VIEW_ITEM_MASK =
@@ -187,15 +189,26 @@
     }
 
     public int getVisibleElements(Launcher launcher) {
-        int flags = HOTSEAT_ICONS | VERTICAL_SWIPE_INDICATOR | TASKBAR;
-        if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                && !launcher.getDeviceProfile().isVerticalBarLayout()) {
+        DeviceProfile deviceProfile = launcher.getDeviceProfile();
+        int flags = WORKSPACE_PAGE_INDICATOR | VERTICAL_SWIPE_INDICATOR | TASKBAR;
+        if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get() && !deviceProfile.isVerticalBarLayout()) {
             flags |= HOTSEAT_SEARCH_BOX;
         }
+        if (!deviceProfile.isTaskbarPresent) {
+            flags |= HOTSEAT_ICONS;
+        }
         return flags;
     }
 
     /**
+     * A shorthand for checking getVisibleElements() & elements == elements.
+     * @return Whether all of the given elements are visible.
+     */
+    public boolean areElementsVisible(Launcher launcher, int elements) {
+        return (getVisibleElements(launcher) & elements) == elements;
+    }
+
+    /**
      * Fraction shift in the vertical translation UI and related properties
      *
      * @see com.android.launcher3.allapps.AllAppsTransitionController
@@ -221,6 +234,13 @@
     }
 
     /**
+     * For this state, whether tasks should layout as a grid rather than a list.
+     */
+    public boolean displayOverviewTasksAsGrid(Launcher launcher) {
+        return false;
+    }
+
+    /**
      * The amount of blur and wallpaper zoom to apply to the background of either the app
      * or Launcher surface in this state. Should be a number between 0 and 1, inclusive.
      *
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index af2d94a..c6766a4 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -219,7 +219,7 @@
     /**
      * Returns the index of the currently displayed page. When in free scroll mode, this is the page
      * that the user was on before entering free scroll mode (e.g. the home screen page they
-     * long-pressed on to enter the overview). Try using {@link #getPageNearestToCenterOfScreen()}
+     * long-pressed on to enter the overview). Try using {@link #getDestinationPage()}
      * to get the page the user is currently scrolling over.
      */
     public int getCurrentPage() {
@@ -1289,7 +1289,7 @@
                     if (((initialScroll >= maxScroll) && (isVelocityLeft || !isFling)) ||
                         ((initialScroll <= minScroll) && (!isVelocityLeft || !isFling))) {
                         mScroller.springBack(initialScroll, minScroll, maxScroll);
-                        mNextPage = getPageNearestToCenterOfScreen();
+                        mNextPage = getDestinationPage();
                     } else {
                         mScroller.setInterpolator(mDefaultInterpolator);
                         mScroller.fling(initialScroll, -velocity,
@@ -1297,11 +1297,12 @@
                             Math.round(getWidth() * 0.5f * OVERSCROLL_DAMP_FACTOR));
 
                         int finalPos = mScroller.getFinalPos();
-                        mNextPage = getPageNearestToCenterOfScreen(finalPos);
+                        mNextPage = getDestinationPage(finalPos);
 
                         int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
                         int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
-                        if (finalPos > minScroll && finalPos < maxScroll) {
+                        if (snapToPageInFreeScroll() && finalPos > minScroll
+                                && finalPos < maxScroll) {
                             // If scrolling ends in the half of the added space that is closer to
                             // the end, settle to the end. Otherwise snap to the nearest page.
                             // If flinging past one of the ends, don't change the velocity as it
@@ -1347,6 +1348,10 @@
         return true;
     }
 
+    protected boolean snapToPageInFreeScroll() {
+        return true;
+    }
+
     protected boolean shouldFlingForVelocity(int velocity) {
         float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
         return Math.abs(velocity) > threshold;
@@ -1452,6 +1457,14 @@
         }
     }
 
+    public int getDestinationPage() {
+        return getDestinationPage(mOrientationHandler.getPrimaryScroll(this));
+    }
+
+    protected int getDestinationPage(int scaledScroll) {
+        return getPageNearestToCenterOfScreen(scaledScroll);
+    }
+
     public int getPageNearestToCenterOfScreen() {
         return getPageNearestToCenterOfScreen(mOrientationHandler.getPrimaryScroll(this));
     }
@@ -1487,7 +1500,7 @@
     }
 
     protected void snapToDestination() {
-        snapToPage(getPageNearestToCenterOfScreen(), getPageSnapDuration());
+        snapToPage(getDestinationPage(), getPageSnapDuration());
     }
 
     protected boolean isInOverScroll() {
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index f60e1f8..ece123d 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -60,7 +60,11 @@
     }
 
     public static int pxFromDp(float size, DisplayMetrics metrics) {
-        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(
-                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+        return pxFromDp(size, metrics, 1f);
+    }
+
+    public static int pxFromDp(float size, DisplayMetrics metrics, float scale) {
+        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(scale
+                * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
 }
diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java
index 7276887..4fd87cb 100644
--- a/src/com/android/launcher3/SecondaryDropTarget.java
+++ b/src/com/android/launcher3/SecondaryDropTarget.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.net.URISyntaxException;
 
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index eab8272..2c24c3a 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -37,6 +37,8 @@
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     private final int[] mTmpCellXY = new int[2];
 
+    private final Rect mTempRect = new Rect();
+
     @ContainerType
     private final int mContainerType;
     private final WallpaperManager mWallpaperManager;
@@ -101,11 +103,12 @@
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
         if (child instanceof LauncherAppWidgetHostView) {
             DeviceProfile profile = mActivity.getDeviceProfile();
+            ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing);
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
         } else {
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
-                    mBorderSpacing);
+                    mBorderSpacing, null);
         }
     }
 
@@ -124,12 +127,12 @@
         final DeviceProfile profile = mActivity.getDeviceProfile();
 
         if (child instanceof LauncherAppWidgetHostView) {
+            ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
-                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing);
-            // Widgets have their own padding
+                    profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
         } else {
             lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
-                    mBorderSpacing);
+                    mBorderSpacing, null);
             // Center the icon/folder
             int cHeight = getCellContentHeight();
             int cellPaddingY = (int) Math.max(0, ((lp.height - cHeight) / 2f));
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 2c45c77..446c873 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -50,6 +50,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.SQLiteCacheHelper;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 2528c03..87fb6fb 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -63,7 +63,6 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Toast;
 
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.Interpolators;
@@ -104,6 +103,8 @@
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.WallpaperOffsetInterpolator;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -416,7 +417,7 @@
                 // widgets as they cannot be placed inside a folder.
                 // Start at the current page and search right (on LTR) until finding a page with
                 // enough space. Since an empty screen is the furthest right, a page must be found.
-                int currentPage = getPageNearestToCenterOfScreen();
+                int currentPage = getDestinationPage();
                 for (int pageIndex = currentPage; pageIndex < getPageCount(); pageIndex++) {
                     CellLayout page = (CellLayout) getPageAt(pageIndex);
                     if (page.hasReorderSolution(dragObject.dragInfo)) {
@@ -445,6 +446,19 @@
         }
 
         updateChildrenLayersEnabled();
+        StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+        stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
+            @Override
+            public void onStateTransitionComplete(LauncherState finalState) {
+                if (finalState == NORMAL) {
+                    if (!mDeferRemoveExtraEmptyScreen) {
+                        removeExtraEmptyScreen(true /* stripEmptyScreens */);
+                    }
+                    stateManager.removeStateListener(this);
+                }
+            }
+        });
+
         mDragInfo = null;
         mOutlineProvider = null;
         mDragSourceInternal = null;
@@ -1876,19 +1890,6 @@
                             };
                         }
                     }
-                    StateManager<LauncherState> stateManager = mLauncher.getStateManager();
-                    stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
-                        @Override
-                        public void onStateTransitionComplete(LauncherState finalState) {
-                            if (finalState == NORMAL) {
-                                if (!mDeferRemoveExtraEmptyScreen) {
-                                    removeExtraEmptyScreen(true /* stripEmptyScreens */);
-                                }
-                                stateManager.removeStateListener(this);
-                            }
-                        }
-                    });
-
                     mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
                             lp.cellX, lp.cellY, item.spanX, item.spanY);
                 } else {
@@ -2972,7 +2973,7 @@
      * @param operators List of operators, in order starting from best matching operator.
      * @return
      */
-    private View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
+    View getFirstMatch(CellLayout[] cellLayouts, final ItemOperator... operators) {
         // This array is filled with the first match for each operator.
         final View[] matches = new View[operators.length];
         // For efficiency, the outer loop should be CellLayout.
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 0e0ddfb..660eeab 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -28,11 +28,13 @@
 import static com.android.launcher3.LauncherState.HINT_STATE;
 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.WORKSPACE_PAGE_INDICATOR;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.ZOOM_OUT;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
 import static com.android.launcher3.graphics.SysUiScrim.SYSUI_PROGRESS;
+import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_HOTSEAT_TRANSLATE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
@@ -141,9 +143,11 @@
             }
 
             float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
-            propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha, fadeInterpolator);
+            propertySetter.setViewAlpha(hotseat, hotseatIconsAlpha,
+                    config.getInterpolator(ANIM_HOTSEAT_FADE, fadeInterpolator));
+            float workspacePageIndicatorAlpha = (elements & WORKSPACE_PAGE_INDICATOR) != 0 ? 1 : 0;
             propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
-                    hotseatIconsAlpha, fadeInterpolator);
+                    workspacePageIndicatorAlpha, fadeInterpolator);
         }
 
         if (config.onlyPlayAtomicComponent()) {
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index ec2a6d5..a92e1aa 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -16,6 +16,8 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
@@ -32,6 +34,7 @@
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -61,6 +64,7 @@
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
@@ -432,9 +436,19 @@
             mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.MAIN);
             findViewById(R.id.tab_personal)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN));
+                    .setOnClickListener((View view) -> {
+                        if (mViewPager.snapToPage(AdapterHolder.MAIN)) {
+                            mLauncher.getStatsLogManager().logger()
+                                    .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
+                        }
+                    });
             findViewById(R.id.tab_work)
-                    .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
+                    .setOnClickListener((View view) -> {
+                        if (mViewPager.snapToPage(AdapterHolder.WORK)) {
+                            mLauncher.getStatsLogManager().logger()
+                                    .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
+                        }
+                    });
             onActivePageChanged(mViewPager.getNextPage());
         } else {
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
@@ -481,6 +495,10 @@
         int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
         View newView = getLayoutInflater().inflate(layout, this, false);
         addView(newView, index);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.WORK_PROFILE_REMOVED, "should show tabs:" + showTabs,
+                    new Exception());
+        }
         if (showTabs) {
             mViewPager = (AllAppsPagedView) newView;
             mViewPager.initParentViews(this);
diff --git a/src/com/android/launcher3/allapps/AllAppsPagedView.java b/src/com/android/launcher3/allapps/AllAppsPagedView.java
index 647402b..14e3b51 100644
--- a/src/com/android/launcher3/allapps/AllAppsPagedView.java
+++ b/src/com/android/launcher3/allapps/AllAppsPagedView.java
@@ -15,9 +15,13 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB;
+
 import android.content.Context;
 import android.util.AttributeSet;
 
+import com.android.launcher3.Launcher;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
@@ -44,4 +48,16 @@
                         R.dimen.all_apps_header_top_padding);
         setPadding(0, topPadding, 0, 0);
     }
+
+    @Override
+    protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
+        boolean resp = super.snapToPageWithVelocity(whichPage, velocity);
+        if (resp && whichPage != mCurrentPage) {
+            Launcher.getLauncher(getContext()).getStatsLogManager().logger()
+                    .log(mCurrentPage < whichPage
+                            ? LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB
+                            : LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB);
+        }
+        return resp;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index e61b95d..ace9938 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -36,7 +36,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsGridAdapter.AppsGridLayoutManager;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 import java.util.ArrayList;
@@ -109,23 +108,6 @@
         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, grid.allAppsCellHeightPx);
     }
 
-    /**
-     * Scrolls this recycler view to the top.
-     */
-    public void scrollToTop() {
-        // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
-        if (mScrollbar != null) {
-            mScrollbar.reattachThumbToScroll();
-        }
-        if (getLayoutManager() instanceof AppsGridLayoutManager) {
-            AppsGridLayoutManager layoutManager = (AppsGridLayoutManager) getLayoutManager();
-            if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
-                // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
-                return;
-            }
-        }
-        scrollToPosition(0);
-    }
 
     @Override
     public void onDraw(Canvas c) {
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index bc2e66c..13ddc12 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -16,8 +16,6 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWITCHED_TO_MAIN_TAB;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_SWITCHED_TO_WORK_TAB;
 
 import android.content.Context;
 import android.graphics.Rect;
@@ -90,14 +88,6 @@
     public void onActivePageChanged(int currentActivePage) {
         super.onActivePageChanged(currentActivePage);
         if (mUsingTabs) {
-            // Log tab switches only when the launcher is in AllApps state
-            if (mLauncher.getStateManager().getCurrentStableState() == LauncherState.ALL_APPS) {
-                mLauncher.getStatsLogManager().logger()
-                        .log(currentActivePage == AdapterHolder.WORK
-                                ? LAUNCHER_ALLAPPS_SWITCHED_TO_WORK_TAB
-                                : LAUNCHER_ALLAPPS_SWITCHED_TO_MAIN_TAB);
-            }
-
             if (currentActivePage == AdapterHolder.WORK) {
                 WorkEduView.showWorkEduIfNeeded(mLauncher);
             } else {
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index f926086..39410a7 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -61,7 +61,8 @@
     /**
      * Called when activity is destroyed. Used to close search system services
      */
-    default void destroy(){}
+    default void destroy() {
+    }
 
     /**
      * Returns true if the QSB should be visible for the given set of visible elements
@@ -75,4 +76,9 @@
      */
     @Nullable
     EditText getEditText();
+
+    /**
+     * sets highlight result's title
+     */
+    default void setFocusedResultTitle(@Nullable  CharSequence title) { }
 }
diff --git a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index cf2e259..3319018 100644
--- a/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -33,10 +33,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
 import com.android.launcher3.util.PackageManagerHelper;
 
-import java.util.ArrayList;
-
 /**
  * An interface to a search box that AllApps can command.
  */
@@ -45,11 +45,11 @@
         OnFocusChangeListener {
 
     protected BaseDraggingActivity mLauncher;
-    protected Callbacks mCb;
+    protected SearchCallback<AdapterItem> mCallback;
     protected ExtendedEditText mInput;
     protected String mQuery;
 
-    protected SearchAlgorithm mSearchAlgorithm;
+    protected SearchAlgorithm<AdapterItem> mSearchAlgorithm;
 
     public void setVisibility(int visibility) {
         mInput.setVisibility(visibility);
@@ -59,9 +59,9 @@
      * Sets the references to the apps model and the search result callback.
      */
     public final void initialize(
-            SearchAlgorithm searchAlgorithm, ExtendedEditText input,
-            BaseDraggingActivity launcher, Callbacks cb) {
-        mCb = cb;
+            SearchAlgorithm<AdapterItem> searchAlgorithm, ExtendedEditText input,
+            BaseDraggingActivity launcher, SearchCallback<AdapterItem> callback) {
+        mCallback = callback;
         mLauncher = launcher;
 
         mInput = input;
@@ -87,10 +87,10 @@
         mQuery = s.toString();
         if (mQuery.isEmpty()) {
             mSearchAlgorithm.cancel(true);
-            mCb.clearSearchResult();
+            mCallback.clearSearchResult();
         } else {
             mSearchAlgorithm.cancel(false);
-            mSearchAlgorithm.doSearch(mQuery, mCb);
+            mSearchAlgorithm.doSearch(mQuery, mCallback);
         }
     }
 
@@ -100,7 +100,7 @@
         }
         // If play store continues auto updating an app, we want to show partial result.
         mSearchAlgorithm.cancel(false);
-        mSearchAlgorithm.doSearch(mQuery, mCb);
+        mSearchAlgorithm.doSearch(mQuery, mCallback);
     }
 
     @Override
@@ -153,7 +153,7 @@
      * Resets the search bar state.
      */
     public void reset() {
-        mCb.clearSearchResult();
+        mCallback.clearSearchResult();
         mInput.reset();
         mQuery = null;
     }
@@ -171,31 +171,4 @@
     public boolean isSearchFieldFocused() {
         return mInput.isFocused();
     }
-
-    /**
-     * Callback for getting search results.
-     */
-    public interface Callbacks {
-
-        /**
-         * Called when the search from primary source is complete.
-         *
-         * @param items sorted list of search result adapter items
-         */
-        void onSearchResult(String query, ArrayList<AdapterItem> items);
-
-        /**
-         * Called when the search from secondary source is complete.
-         *
-         * @param items sorted list of search result adapter items
-         */
-        void onAppendSearchResult(String query, ArrayList<AdapterItem> items);
-
-        /**
-         * Called when the search results should be cleared.
-         */
-        void clearSearchResult();
-    }
-
-
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index aef32d7..426fd0c 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -47,6 +47,7 @@
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.search.SearchCallback;
 
 import java.util.ArrayList;
 
@@ -54,7 +55,7 @@
  * Layout to contain the All-apps search UI.
  */
 public class AppsSearchContainerLayout extends ExtendedEditText
-        implements SearchUiManager, AllAppsSearchBarController.Callbacks,
+        implements SearchUiManager, SearchCallback<AdapterItem>,
         AllAppsStore.OnUpdateListener, Insettable {
 
     private final BaseDraggingActivity mLauncher;
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 66bbd2e..4e213b0 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -19,14 +19,17 @@
 import android.os.Handler;
 
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.allapps.AllAppsGridAdapter.AdapterItem;
 import com.android.launcher3.model.data.AppInfo;
+import com.android.launcher3.search.SearchAlgorithm;
+import com.android.launcher3.search.SearchCallback;
 
 import java.text.Collator;
 
 /**
  * The default search implementation.
  */
-public class DefaultAppSearchAlgorithm implements SearchAlgorithm {
+public class DefaultAppSearchAlgorithm implements SearchAlgorithm<AdapterItem> {
 
     protected final Handler mResultHandler;
     private final AppsSearchPipeline mAppsSearchPipeline;
@@ -45,7 +48,7 @@
 
     @Override
     public void doSearch(final String query,
-            final AllAppsSearchBarController.Callbacks callback) {
+            final SearchCallback<AdapterItem> callback) {
         mAppsSearchPipeline.query(query,
                 results -> mResultHandler.post(
                         () -> callback.onSearchResult(query, results)),
diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
index 4ef154e..76099a6 100644
--- a/src/com/android/launcher3/allapps/search/LiveSearchManager.java
+++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java
@@ -43,11 +43,11 @@
 import androidx.slice.SliceViewManager.SliceCallback;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 8016b2d..6b9ed09 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -16,9 +16,6 @@
 
 package com.android.launcher3.anim;
 
-import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
-
-import android.content.Context;
 import android.graphics.Path;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.AccelerateInterpolator;
@@ -67,9 +64,6 @@
      */
     public static final Interpolator FINAL_FRAME = t -> t < 1 ? 0 : 1;
 
-    private static final int MIN_SETTLE_DURATION = 200;
-    private static final float OVERSHOOT_FACTOR = 0.9f;
-
     static {
         Path exaggeratedEase = new Path();
         exaggeratedEase.moveTo(0, 0);
@@ -175,76 +169,4 @@
             float upperBound) {
         return t -> Utilities.mapRange(interpolator.getInterpolation(t), lowerBound, upperBound);
     }
-
-    /**
-     * Computes parameters necessary for an overshoot effect.
-     */
-    public static class OvershootParams {
-        public Interpolator interpolator;
-        public float start;
-        public float end;
-        public long duration;
-
-        /**
-         * Given the input params, sets OvershootParams variables to be used by the caller.
-         * @param startProgress The progress from 0 to 1 that the overshoot starts from.
-         * @param overshootPastProgress The progress from 0 to 1 where we overshoot past (should
-         *        either be equal to startProgress or endProgress, depending on if we want to
-         *        overshoot immediately or only once we reach the end).
-         * @param endProgress The final progress from 0 to 1 that we will settle to.
-         * @param velocityPxPerMs The initial velocity that causes this overshoot.
-         * @param totalDistancePx The distance against which progress is calculated.
-         */
-        public OvershootParams(float startProgress, float overshootPastProgress,
-                float endProgress, float velocityPxPerMs, int totalDistancePx, Context context) {
-            velocityPxPerMs = Math.abs(velocityPxPerMs);
-            overshootPastProgress = Math.max(overshootPastProgress, startProgress);
-            start = startProgress;
-            int startPx = (int) (start * totalDistancePx);
-            // Overshoot by about half a frame.
-            float overshootBy = OVERSHOOT_FACTOR * velocityPxPerMs *
-                    getSingleFrameMs(context) / totalDistancePx / 2;
-            overshootBy = Utilities.boundToRange(overshootBy, 0.02f, 0.15f);
-            end = overshootPastProgress + overshootBy;
-            int endPx = (int) (end  * totalDistancePx);
-            int overshootDistance = endPx - startPx;
-            // Calculate deceleration necessary to reach overshoot distance.
-            // Formula: velocityFinal^2 = velocityInitial^2 + 2 * acceleration * distance
-            //          0 = v^2 + 2ad (velocityFinal == 0)
-            //          a = v^2 / -2d
-            float decelerationPxPerMs = velocityPxPerMs * velocityPxPerMs / (2 * overshootDistance);
-            // Calculate time necessary to reach peak of overshoot.
-            // Formula: acceleration = velocity / time
-            //          time = velocity / acceleration
-            duration = (long) (velocityPxPerMs / decelerationPxPerMs);
-
-            // Now that we're at the top of the overshoot, need to settle back to endProgress.
-            float settleDistance = end - endProgress;
-            int settleDistancePx = (int) (settleDistance * totalDistancePx);
-            // Calculate time necessary for the settle.
-            // Formula: distance = velocityInitial * time + 1/2 * acceleration * time^2
-            //          d = 1/2at^2 (velocityInitial = 0, since we just stopped at the top)
-            //          t = sqrt(2d/a)
-            // Above formula assumes constant acceleration. Since we use ACCEL_DEACCEL, we actually
-            // have acceleration to halfway then deceleration the rest. So the formula becomes:
-            //          t = sqrt(d/a) * 2 (half the distance for accel, half for deaccel)
-            long settleDuration = (long) Math.sqrt(settleDistancePx / decelerationPxPerMs) * 4;
-
-            settleDuration = Math.max(MIN_SETTLE_DURATION, settleDuration);
-            // How much of the animation to devote to playing the overshoot (the rest is for settle).
-            float overshootFraction = (float) duration / (duration + settleDuration);
-            duration += settleDuration;
-            // Finally, create the interpolator, composed of two interpolators: an overshoot, which
-            // reaches end > 1, and then a settle to endProgress.
-            Interpolator overshoot = Interpolators.clampToProgress(DEACCEL, 0, overshootFraction);
-            // The settle starts at 1, where 1 is the top of the overshoot, and maps to a fraction
-            // such that final progress is endProgress. For example, if we overshot to 1.1 but want
-            // to end at 1, we need to map to 1/1.1.
-            Interpolator settle = Interpolators.clampToProgress(Interpolators.mapToProgress(
-                    ACCEL_DEACCEL, 1, (endProgress - start) / (end - start)), overshootFraction, 1);
-            interpolator = t -> t <= overshootFraction
-                    ? overshoot.getInterpolation(t)
-                    : settle.getInterpolation(t);
-        }
-    }
 }
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index ef7bdf4..e406e9b 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -158,7 +158,7 @@
             "ENABLE_SMARTSPACE_UNIVERSAL", false,
             "Replace Smartspace with a version rendered by System UI.");
 
-    public static final BooleanFlag ENABLE_SMARTSPACE_ENHANCED = getDebugFlag(
+    public static final BooleanFlag ENABLE_SMARTSPACE_ENHANCED = new DeviceFlag(
             "ENABLE_SMARTSPACE_ENHANCED", false,
             "Replace Smartspace with the enhanced version. "
               + "Ignored if ENABLE_SMARTSPACE_UNIVERSAL is enabled.");
@@ -263,6 +263,8 @@
         }
 
         public void addChangeListener(Context context, Runnable r) { }
+
+        public void removeChangeListener(Runnable r) {}
     }
 
     public static class DebugFlag extends BooleanFlag {
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 77d2b85..5dc94ff 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -47,8 +47,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -56,6 +54,8 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.PinRequestHelper;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
diff --git a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
index 7788f93..0a1aba1 100644
--- a/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
+++ b/src/com/android/launcher3/dragndrop/FlingToDeleteHelper.java
@@ -65,6 +65,9 @@
     }
 
     public Runnable getFlingAnimation(DropTarget.DragObject dragObject, DragOptions options) {
+        if (options == null) {
+            return null;
+        }
         PointF vel = isFlingingToDelete();
         options.isFlingToDelete = vel != null;
         if (!options.isFlingToDelete) {
diff --git a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
index be6a07f..fa062f5 100644
--- a/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
+++ b/src/com/android/launcher3/dragndrop/LivePreviewWidgetCell.java
@@ -12,10 +12,10 @@
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.model.WidgetItem;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetCell;
 
 /**
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 6104d80..2290473 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -30,8 +30,8 @@
 
 import com.android.launcher3.DragSource;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PendingAddItemInfo;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingItemDragHelper;
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 504b29e..bcb3a54 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -44,6 +44,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.TypedValue;
 import android.view.FocusFinder;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -246,11 +247,16 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        final DeviceProfile dp = mLauncher.getDeviceProfile();
+        final int paddingLeftRight = dp.folderContentPaddingLeftRight;
+
         mContent = findViewById(R.id.folder_content);
+        mContent.setPadding(paddingLeftRight, dp.folderContentPaddingTop, paddingLeftRight, 0);
         mContent.setFolder(this);
 
         mPageIndicator = findViewById(R.id.folder_page_indicator);
         mFolderName = findViewById(R.id.folder_name);
+        mFolderName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.folderLabelTextSizePx);
         mFolderName.setOnBackKeyListener(this);
         mFolderName.setOnFocusChangeListener(this);
         mFolderName.setOnEditorActionListener(this);
@@ -262,12 +268,7 @@
         mFolderName.forceDisableSuggestions(true);
 
         mFooter = findViewById(R.id.folder_footer);
-
-        // We find out how tall footer wants to be (it is set to wrap_content), so that
-        // we can allocate the appropriate amount of space for it.
-        int measureSpec = MeasureSpec.UNSPECIFIED;
-        mFooter.measure(measureSpec, measureSpec);
-        mFooterHeight = mFooter.getMeasuredHeight();
+        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
 
         if (Utilities.ATLEAST_R) {
             mFolderWindowInsetsAnimationCallback =
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 1cac31e..feb528c 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -324,7 +324,9 @@
 
             final int previewPosX =
                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
-            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
+            final float paddingTop = btv.getPaddingTop() * iconScale;
+            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop)
+                    / folderScale);
 
             final float xDistance = previewPosX - btvLp.x;
             final float yDistance = previewPosY - btvLp.y;
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 3a9986e..5e9b179 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -58,7 +58,6 @@
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
@@ -91,6 +90,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 431d534..d554bb9 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -362,11 +362,11 @@
         @UiEvent(doc = "User closed the AllApps keyboard.")
         LAUNCHER_ALLAPPS_KEYBOARD_CLOSED(694),
 
-        @UiEvent(doc = "User switched to Main tab in AllApps screen.")
-        LAUNCHER_ALLAPPS_SWITCHED_TO_MAIN_TAB(695),
+        @UiEvent(doc = "User switched to AllApps Main/Personal tab by swiping left.")
+        LAUNCHER_ALLAPPS_SWIPE_TO_PERSONAL_TAB(695),
 
-        @UiEvent(doc = "User switched to Work tab in AllApps screen.")
-        LAUNCHER_ALLAPPS_SWITCHED_TO_WORK_TAB(696),
+        @UiEvent(doc = "User switched to AllApps Work tab by swiping right.")
+        LAUNCHER_ALLAPPS_SWIPE_TO_WORK_TAB(696),
 
         @UiEvent(doc = "Default event when dedicated UI event is not available for the user action"
                 + " on slice .")
@@ -396,8 +396,17 @@
         @UiEvent(doc = "IME is used for selecting the focused item on the AllApps screen.")
         LAUNCHER_ALLAPPS_FOCUSED_ITEM_SELECTED_WITH_IME(718),
 
+        @UiEvent(doc = "User long-pressed on an AllApps item.")
+        LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED(719),
+
         @UiEvent(doc = "Launcher entered into AllApps state with device search enabled.")
         LAUNCHER_ALLAPPS_ENTRY_WITH_DEVICE_SEARCH(720),
+
+        @UiEvent(doc = "User switched to AllApps Main/Personal tab by tapping on it.")
+        LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB(721),
+
+        @UiEvent(doc = "User switched to AllApps Work tab by tapping on it.")
+        LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB(722),
         ;
 
         // ADD MORE
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index e8a52bd..804e72e 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -25,7 +25,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
@@ -40,6 +39,7 @@
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 79467d3..c6c0791 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -39,7 +39,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.graphics.LauncherPreviewRenderer;
@@ -48,6 +47,7 @@
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
diff --git a/src/com/android/launcher3/model/ItemInstallQueue.java b/src/com/android/launcher3/model/ItemInstallQueue.java
index d09bf81..df8367f 100644
--- a/src/com/android/launcher3/model/ItemInstallQueue.java
+++ b/src/com/android/launcher3/model/ItemInstallQueue.java
@@ -42,7 +42,6 @@
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -52,6 +51,7 @@
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PersistedItemArray;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import java.util.HashSet;
 import java.util.List;
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 8438622..f6c7c06 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -45,12 +45,12 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
 import android.util.TimingLogger;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
@@ -87,6 +87,7 @@
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.ArrayList;
@@ -126,7 +127,7 @@
 
     private final UserManagerState mUserManagerState = new UserManagerState();
 
-    protected Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap;
+    protected final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap = new ArrayMap<>();
 
     private boolean mStopped;
 
@@ -191,6 +192,16 @@
             loadWorkspace(allShortcuts);
             logger.addSplit("loadWorkspace");
 
+            // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
+            // sanitizeData should not be invoked if the workspace is loaded from a db different
+            // from the main db as defined in the invariant device profile.
+            // (e.g. both grid preview and minimal device mode uses a different db)
+            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
+                verifyNotStopped();
+                sanitizeData();
+                logger.addSplit("sanitizeData");
+            }
+
             verifyNotStopped();
             mResults.bindWorkspace();
             logger.addSplit("bindWorkspace");
@@ -273,14 +284,6 @@
                 loadFolderNames();
             }
 
-            // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
-            // sanitizeData should not be invoked if the workspace is loaded from a db different
-            // from the main db as defined in the invariant device profile.
-            // (e.g. both grid preview and minimal device mode uses a different db)
-            if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
-                sanitizeData();
-            }
-
             verifyNotStopped();
             updateHandler.finish();
             logger.addSplit("finish icon update");
@@ -664,12 +667,13 @@
                             final boolean wasProviderReady = !c.hasRestoreFlag(
                                     LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
 
-                            if (mWidgetProvidersMap == null) {
-                                mWidgetProvidersMap = WidgetManagerHelper.getAllProvidersMap(
-                                        context);
+                            ComponentKey providerKey = new ComponentKey(component, c.user);
+                            if (!mWidgetProvidersMap.containsKey(providerKey)) {
+                                mWidgetProvidersMap.put(providerKey,
+                                        widgetHelper.findProvider(component, c.user));
                             }
-                            final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(
-                                    new ComponentKey(component, c.user));
+                            final AppWidgetProviderInfo provider =
+                                    mWidgetProvidersMap.get(providerKey);
 
                             final boolean isProviderReady = isValidProvider(provider);
                             if (!isSafeMode && !customWidget &&
@@ -874,10 +878,10 @@
                 }
             }
 
-            // Remove any ghost widgets
-            LauncherSettings.Settings.call(contentResolver,
-                    LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
         }
+        // Remove any ghost widgets
+        LauncherSettings.Settings.call(contentResolver,
+                LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS);
 
         // Update pinned state of model shortcuts
         mBgDataModel.updateShortcutPinnedState(context);
diff --git a/src/com/android/launcher3/model/ModelDelegate.java b/src/com/android/launcher3/model/ModelDelegate.java
index 92bea5b..13ec1ec 100644
--- a/src/com/android/launcher3/model/ModelDelegate.java
+++ b/src/com/android/launcher3/model/ModelDelegate.java
@@ -27,6 +27,8 @@
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ResourceBasedOverride;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.util.Map;
 
 /**
@@ -89,4 +91,11 @@
     @WorkerThread
     public void destroy() { }
 
+    /**
+     * Add data to a dumpsys request for Launcher (e.g. for bug reports).
+     *
+     * @see com.android.launcher3.Launcher#dump(java.lang.String, java.io.FileDescriptor,
+     *                                          java.io.PrintWriter, java.lang.String[])
+     **/
+    public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { }
 }
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index f7b43d6..312435d 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -28,7 +28,6 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherProvider;
 import com.android.launcher3.LauncherSettings;
@@ -43,6 +42,7 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
index 434776c..c0dc34a 100644
--- a/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageIncrementalDownloadUpdatedTask.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import android.content.Intent;
 import android.os.UserHandle;
 
 import com.android.launcher3.LauncherAppState;
@@ -66,8 +65,7 @@
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItems = new ArrayList<>();
         synchronized (dataModel) {
             dataModel.forAllWorkspaceItemInfos(mUser, si -> {
-                Intent intent = si.getIntent();
-                if ((intent != null) && mPackageName.equals(intent.getPackage())) {
+                if (mPackageName.equals(si.getTargetPackage())) {
                     si.runtimeStatusFlags &= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
                     si.setProgressLevel(downloadInfo);
                     updatedWorkspaceItems.add(si);
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 1380e9e..9889a80 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model;
 
-import android.content.Intent;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 
@@ -72,9 +71,8 @@
         synchronized (dataModel) {
             final HashSet<ItemInfo> updates = new HashSet<>();
             dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> {
-                Intent intent = si.getIntent();
-                if (si.hasPromiseIconUi() && (intent != null)
-                        && mInstallInfo.packageName.equals(intent.getPackage())) {
+                if (si.hasPromiseIconUi()
+                        && mInstallInfo.packageName.equals(si.getTargetPackage())) {
                     int installProgress = mInstallInfo.progress;
 
                     si.setProgressLevel(installProgress, PackageInstallInfo.STATUS_INSTALLING);
diff --git a/src/com/android/launcher3/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index de2481a..97071bb 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -4,11 +4,11 @@
 import android.content.pm.PackageManager;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 /**
  * An wrapper over various items displayed in a widget picker,
diff --git a/src/com/android/launcher3/model/data/ItemInfo.java b/src/com/android/launcher3/model/data/ItemInfo.java
index 00ac12f..e54f1e7 100644
--- a/src/com/android/launcher3/model/data/ItemInfo.java
+++ b/src/com/android/launcher3/model/data/ItemInfo.java
@@ -184,6 +184,24 @@
         return Optional.ofNullable(getIntent()).map(Intent::getComponent).orElse(mComponentName);
     }
 
+    /**
+     * Returns this item's package name.
+     *
+     * Prioritizes the component package name, then uses the intent package name as a fallback.
+     * This ensures deep shortcuts are supported.
+     */
+    @Nullable
+    public String getTargetPackage() {
+        ComponentName component = getTargetComponent();
+        Intent intent = getIntent();
+
+        return component != null
+                ? component.getPackageName()
+                : intent != null
+                        ? intent.getPackage()
+                        : null;
+    }
+
     public void writeToValues(ContentWriter writer) {
         writer.put(LauncherSettings.Favorites.ITEM_TYPE, itemType)
                 .put(LauncherSettings.Favorites.CONTAINER, container)
diff --git a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
index 408796f..6189dc9 100644
--- a/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
+++ b/src/com/android/launcher3/pageindicators/WorkspacePageIndicator.java
@@ -268,7 +268,9 @@
         } else {
             lp.leftMargin = lp.rightMargin = 0;
             lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
-            lp.bottomMargin = grid.hotseatBarSizePx + insets.bottom;
+            lp.bottomMargin = grid.isTaskbarPresent
+                    ? grid.workspacePadding.bottom + insets.bottom
+                    : grid.hotseatBarSizePx + insets.bottom;
         }
         setLayoutParams(lp);
     }
diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java
index 56438d0..5a34d2a 100644
--- a/src/com/android/launcher3/popup/ArrowPopup.java
+++ b/src/com/android/launcher3/popup/ArrowPopup.java
@@ -26,11 +26,8 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
 import android.graphics.Outline;
-import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.view.Gravity;
@@ -51,7 +48,6 @@
 import com.android.launcher3.anim.RevealOutlineAnimation;
 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 
@@ -72,7 +68,11 @@
     protected final T mLauncher;
     protected final boolean mIsRtl;
 
-    private final int mArrowOffset;
+    private final int mArrowOffsetVertical;
+    private final int mArrowOffsetHorizontal;
+    private final int mArrowWidth;
+    private final int mArrowHeight;
+    private final int mArrowPointRadius;
     private final View mArrow;
 
     protected boolean mIsLeftAligned;
@@ -103,11 +103,14 @@
 
         // Initialize arrow view
         final Resources resources = getResources();
-        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
-        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
+        mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
+        mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
         mArrow = new View(context);
-        mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
-        mArrowOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+        mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
+        mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
+        mArrowOffsetHorizontal = resources.getDimensionPixelSize(
+                R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
+        mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
     }
 
     public ArrowPopup(Context context, AttributeSet attrs) {
@@ -200,48 +203,33 @@
         orientAboutObject();
     }
 
-    private void addArrow() {
-        final Resources res = getResources();
-        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
-                ? R.dimen.popup_arrow_horizontal_center_start
-                : R.dimen.popup_arrow_horizontal_center_end);
-        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
-        getPopupContainer().addView(mArrow);
-        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+    private int getArrowLeft() {
         if (mIsLeftAligned) {
-            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
-        } else {
-            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
+            return mArrowOffsetHorizontal;
         }
+        return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
+    }
+
+    private void addArrow() {
+        getPopupContainer().addView(mArrow);
+        mArrow.setX(getX() + getArrowLeft());
 
         if (Gravity.isVertical(mGravity)) {
             // This is only true if there wasn't room for the container next to the icon,
             // so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
             mArrow.setVisibility(INVISIBLE);
         } else {
-            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                    arrowLp.width, arrowLp.height, !mIsAboveIcon));
-            Paint arrowPaint = arrowDrawable.getPaint();
-            arrowPaint.setColor(Themes.getAttrColor(getContext(), R.attr.popupColorPrimary));
-            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
-            arrowPaint.setPathEffect(new CornerPathEffect(radius));
-            mArrow.setBackground(arrowDrawable);
-            // Clip off the part of the arrow that is underneath the popup.
-            if (mIsAboveIcon) {
-                mArrow.setClipBounds(new Rect(0, -mArrowOffset, arrowLp.width, arrowLp.height));
-            } else {
-                mArrow.setClipBounds(new Rect(0, 0, arrowLp.width, arrowLp.height + mArrowOffset));
-            }
+            mArrow.setBackground(new RoundedArrowDrawable(
+                    mArrowWidth, mArrowHeight, mArrowPointRadius,
+                    mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
+                    mArrowOffsetHorizontal, -mArrowOffsetVertical,
+                    !mIsAboveIcon, mIsLeftAligned,
+                    Themes.getAttrColor(getContext(), R.attr.popupColorPrimary)));
             mArrow.setElevation(getElevation());
         }
 
-        mArrow.setPivotX(arrowLp.width / 2);
-        mArrow.setPivotY(mIsAboveIcon ? arrowLp.height : 0);
-    }
-
-    protected boolean isAlignedWithStart() {
-        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+        mArrow.setPivotX(mArrowWidth / 2.0f);
+        mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
     }
 
     /**
@@ -274,8 +262,9 @@
      */
     private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
         int width = getMeasuredWidth();
-        int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset
+        int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical
                 + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
         int height = getMeasuredHeight() + extraVerticalSpace;
 
@@ -291,22 +280,7 @@
 
         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
         int iconWidth = mTempRect.width();
-        Resources resources = getResources();
-        int xOffset;
-        if (isAlignedWithStart()) {
-            // Aligning with the shortcut icon.
-            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
-            int shortcutPaddingStart = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_start);
-            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
-        } else {
-            // Aligning with the drag handle.
-            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
-                    R.dimen.deep_shortcut_drag_handle_size);
-            int shortcutPaddingEnd = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_end);
-            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
-        }
+        int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2;
         x += mIsLeftAligned ? xOffset : -xOffset;
 
         // Check whether we can still align as we originally wanted, now that we've calculated x.
@@ -375,12 +349,14 @@
         FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams();
         if (mIsAboveIcon) {
             arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
-            lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
-            arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffset - insets.bottom;
+            lp.bottomMargin =
+                    getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
+            arrowLp.bottomMargin =
+                    lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom;
         } else {
             arrowLp.gravity = lp.gravity = Gravity.TOP;
             lp.topMargin = y + insets.top;
-            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffset;
+            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical;
         }
     }
 
@@ -529,22 +505,13 @@
     protected void onCreateCloseAnimation(AnimatorSet anim) { }
 
     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
-        Resources res = getResources();
-        int arrowCenterX = res.getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
-                R.dimen.popup_arrow_horizontal_center_start:
-                R.dimen.popup_arrow_horizontal_center_end);
-        int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
-        float arrowCornerRadius = res.getDimension(R.dimen.popup_arrow_corner_radius);
-        if (!mIsLeftAligned) {
-            arrowCenterX = getMeasuredWidth() - arrowCenterX;
-        }
+        int arrowLeft = getArrowLeft();
         int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
 
-        mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth,
-                arrowCenterY);
+        mStartRect.set(arrowLeft, arrowCenterY, arrowLeft + mArrowWidth, arrowCenterY);
 
-        return new RoundedRectRevealOutlineProvider
-                (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect);
+        return new RoundedRectRevealOutlineProvider(
+                mArrowPointRadius, mOutlineRadius, mStartRect, mEndRect);
     }
 
     /**
diff --git a/src/com/android/launcher3/popup/RoundedArrowDrawable.java b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
new file mode 100644
index 0000000..e662d5c
--- /dev/null
+++ b/src/com/android/launcher3/popup/RoundedArrowDrawable.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 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.popup;
+
+import static java.lang.Math.atan;
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toDegrees;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable for a very specific purpose. Used for the caret arrow on a rounded rectangle popup
+ * bubble.
+ * Draws a triangle with one rounded tip, the opposite edge is clipped by the body of the popup
+ * so there is no overlap when drawing them together.
+ */
+public class RoundedArrowDrawable extends Drawable {
+
+    private final Path mPath;
+    private final Paint mPaint;
+
+    /**
+     * Default constructor.
+     *
+     * @param width of the arrow.
+     * @param height of the arrow.
+     * @param radius of the tip of the arrow.
+     * @param popupRadius of the rect to clip this by.
+     * @param popupWidth of the rect to clip this by.
+     * @param popupHeight of the rect to clip this by.
+     * @param arrowOffsetX from the edge of the popup to the arrow.
+     * @param arrowOffsetY how much the arrow will overlap the popup.
+     * @param isPointingUp or not.
+     * @param leftAligned or false for right aligned.
+     * @param color to draw the triangle.
+     */
+    public RoundedArrowDrawable(float width, float height, float radius, float popupRadius,
+            float popupWidth, float popupHeight,
+            float arrowOffsetX, float arrowOffsetY, boolean isPointingUp, boolean leftAligned,
+            int color) {
+        mPath = new Path();
+        mPaint = new Paint();
+        mPaint.setColor(color);
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setAntiAlias(true);
+
+        // Make the drawable with the triangle pointing down and positioned on the left..
+        addDownPointingRoundedTriangleToPath(width, height, radius, mPath);
+        clipPopupBodyFromPath(popupRadius, popupWidth, popupHeight, arrowOffsetX, arrowOffsetY,
+                mPath);
+
+        // ... then flip it horizontal or vertical based on where it will be used.
+        Matrix pathTransform = new Matrix();
+        pathTransform.setScale(
+                leftAligned ? 1 : -1, isPointingUp ? -1 : 1, width * 0.5f, height * 0.5f);
+        mPath.transform(pathTransform);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        canvas.drawPath(mPath, mPaint);
+    }
+
+    @Override
+    public void getOutline(Outline outline) {
+        outline.setPath(mPath);
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int i) {
+        mPaint.setAlpha(i);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+    }
+
+    private static void addDownPointingRoundedTriangleToPath(float width, float height,
+            float radius, Path path) {
+        // Calculated for the arrow pointing down, will be flipped later if needed.
+
+        // Theta is half of the angle inside the triangle tip
+        float tanTheta = width / (2.0f * height);
+        float theta = (float) atan(tanTheta);
+
+        // Some trigonometry to find the center of the circle for the rounded tip
+        float roundedPointCenterY = (float) (height - (radius / sin(theta)));
+
+        // p is the distance along the triangle side to the intersection with the point circle
+        float p = radius / tanTheta;
+        float lineRoundPointIntersectFromCenter = (float) (p * sin(theta));
+        float lineRoundPointIntersectFromTop = (float) (height - (p * cos(theta)));
+
+        float centerX = width / 2.0f;
+        float thetaDeg = (float) toDegrees(theta);
+
+        path.reset();
+        path.moveTo(0, 0);
+        // Draw the top
+        path.lineTo(width, 0);
+        // Draw the right side up to the circle intersection
+        path.lineTo(
+                centerX + lineRoundPointIntersectFromCenter,
+                lineRoundPointIntersectFromTop);
+        // Draw the rounded point
+        path.arcTo(
+                centerX - radius,
+                roundedPointCenterY - radius,
+                centerX + radius,
+                roundedPointCenterY + radius,
+                thetaDeg,
+                180 - (2 * thetaDeg),
+                false);
+        // Draw the left edge to close
+        path.lineTo(0, 0);
+        path.close();
+    }
+
+    private static void clipPopupBodyFromPath(float popupRadius, float popupWidth,
+            float popupHeight, float arrowOffsetX, float arrowOffsetY, Path path) {
+        // Make a path that is used to clip the triangle, this represents the body of the popup
+        Path clipPiece = new Path();
+        clipPiece.addRoundRect(
+                0, 0, popupWidth, popupHeight,
+                popupRadius, popupRadius, Path.Direction.CW);
+        // clipping is performed as if the arrow is pointing down and positioned on the left, the
+        // resulting path will be flipped as needed later.
+        // The extra 0.5 in the vertical offset is to close the gap between this anti-aliased object
+        // and the anti-aliased body of the popup.
+        clipPiece.offset(-arrowOffsetX, -popupHeight + arrowOffsetY - 0.5f);
+        path.op(clipPiece, Path.Op.DIFFERENCE);
+    }
+}
diff --git a/src/com/android/launcher3/qsb/QsbContainerView.java b/src/com/android/launcher3/qsb/QsbContainerView.java
index 459aefe..a191df4 100644
--- a/src/com/android/launcher3/qsb/QsbContainerView.java
+++ b/src/com/android/launcher3/qsb/QsbContainerView.java
@@ -32,11 +32,11 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.util.AttributeSet;
+import android.util.SizeF;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -299,7 +299,7 @@
             InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
 
             Bundle opts = new Bundle();
-            ArrayList<PointF> sizes = AppWidgetResizeFrame
+            ArrayList<SizeF> sizes = AppWidgetResizeFrame
                     .getWidgetSizes(getContext(), idp.numColumns, 1);
             Rect size = AppWidgetResizeFrame.getMinMaxSizes(sizes, null /* outRect */);
             opts.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, size.left);
diff --git a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java b/src/com/android/launcher3/search/SearchAlgorithm.java
similarity index 76%
rename from src/com/android/launcher3/allapps/search/SearchAlgorithm.java
rename to src/com/android/launcher3/search/SearchAlgorithm.java
index c409b1c..1665354 100644
--- a/src/com/android/launcher3/allapps/search/SearchAlgorithm.java
+++ b/src/com/android/launcher3/search/SearchAlgorithm.java
@@ -13,17 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps.search;
+package com.android.launcher3.search;
 
 /**
  * An interface for handling search.
+ *
+ * @param <T> Search Result type
  */
-public interface SearchAlgorithm {
+public interface SearchAlgorithm<T> {
 
     /**
-     * Performs search and sends the result to the callback.
+     * Performs search and sends the result to {@link SearchCallback}.
      */
-    void doSearch(String query, AllAppsSearchBarController.Callbacks callback);
+    void doSearch(String query, SearchCallback<T> callback);
 
     /**
      * Cancels any active request.
diff --git a/src/com/android/launcher3/search/SearchCallback.java b/src/com/android/launcher3/search/SearchCallback.java
new file mode 100644
index 0000000..5796116
--- /dev/null
+++ b/src/com/android/launcher3/search/SearchCallback.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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.search;
+
+import java.util.ArrayList;
+
+/**
+ * An interface for receiving search results.
+ *
+ * @param <T> Search Result type
+ */
+public interface SearchCallback<T> {
+
+    /**
+     * Called when the search from primary source is complete.
+     *
+     * @param items list of search results
+     */
+    void onSearchResult(String query, ArrayList<T> items);
+
+    /**
+     * Called when the search from secondary source is complete.
+     *
+     * @param items list of search results
+     */
+    void onAppendSearchResult(String query, ArrayList<T> items);
+
+    /**
+     * Called when the search results should be cleared.
+     */
+    void clearSearchResult();
+}
+
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index ac8dac5..f03065c 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -129,6 +129,7 @@
         private String mHighLightKey;
         private boolean mPreferenceHighlighted = false;
         private NotificationDotsPreference mNotificationSettingsChangedListener;
+        private Preference mDeveloperOptionPref;
 
         @Override
         public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -202,18 +203,37 @@
                     return FeatureFlags.showFlagTogglerUi(getContext());
 
                 case DEVELOPER_OPTIONS_KEY:
-                    // Show if plugins are enabled or flag UI is enabled.
-                    return FeatureFlags.showFlagTogglerUi(getContext()) ||
-                            PluginManagerWrapper.hasPlugins(getContext());
+                    mDeveloperOptionPref = preference;
+                    return updateDeveloperOption();
             }
 
             return true;
         }
 
+        /**
+         * Show if plugins are enabled or flag UI is enabled.
+         * @return True if we should show the preference option.
+         */
+        private boolean updateDeveloperOption() {
+            boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
+                    || PluginManagerWrapper.hasPlugins(getContext());
+            if (mDeveloperOptionPref != null) {
+                mDeveloperOptionPref.setEnabled(showPreference);
+                if (showPreference) {
+                    getPreferenceScreen().addPreference(mDeveloperOptionPref);
+                } else {
+                    getPreferenceScreen().removePreference(mDeveloperOptionPref);
+                }
+            }
+            return showPreference;
+        }
+
         @Override
         public void onResume() {
             super.onResume();
 
+            updateDeveloperOption();
+
             if (isAdded() && !mPreferenceHighlighted) {
                 PreferenceHighlighter highlighter = createHighlighter();
                 if (highlighter != null) {
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index beb5b68..a18f340 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -302,10 +302,6 @@
     }
 
     private PendingAnimation createAnimationToNewWorkspaceInternal(final STATE_TYPE state) {
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "createAnimationToNewWorkspaceInternal: "
-                    + state);
-        }
         PendingAnimation builder = new PendingAnimation(mConfig.duration);
         if (mConfig.getAnimComponents() != 0) {
             for (StateHandler handler : getStateHandlers()) {
@@ -328,9 +324,6 @@
 
             @Override
             public void onAnimationSuccess(Animator animator) {
-                if (TestProtocol.sDebugTracing) {
-                    Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "onAnimationSuccess: " + state);
-                }
                 onStateTransitionEnd(state);
             }
         };
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 44bcc34..fce8fff 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -93,6 +93,6 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        return super.getVisibleElements(launcher) & ~TASKBAR;
+        return (super.getVisibleElements(launcher) | HOTSEAT_ICONS) & ~TASKBAR;
     }
 }
diff --git a/src/com/android/launcher3/states/StateAnimationConfig.java b/src/com/android/launcher3/states/StateAnimationConfig.java
index ec949eb..e4c67ee 100644
--- a/src/com/android/launcher3/states/StateAnimationConfig.java
+++ b/src/com/android/launcher3/states/StateAnimationConfig.java
@@ -75,6 +75,7 @@
             ANIM_DEPTH,
             ANIM_OVERVIEW_ACTIONS_FADE,
             ANIM_TASKBAR_FADE,
+            ANIM_HOTSEAT_FADE,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimType {}
@@ -95,8 +96,9 @@
     public static final int ANIM_DEPTH = 14;
     public static final int ANIM_OVERVIEW_ACTIONS_FADE = 15;
     public static final int ANIM_TASKBAR_FADE = 16;
+    public static final int ANIM_HOTSEAT_FADE = 17; // if not set, falls back to ANIM_WORKSPACE_FADE
 
-    private static final int ANIM_TYPES_COUNT = 17;
+    private static final int ANIM_TYPES_COUNT = 18;
 
     protected final Interpolator[] mInterpolators = new Interpolator[ANIM_TYPES_COUNT];
 
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 218172b..7cb6e34 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -103,7 +103,8 @@
     public static final String REQUEST_MOCK_SENSOR_ROTATION = "mock-sensor-rotation";
 
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
-    public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
     public static final String WORK_PROFILE_REMOVED = "b/159671700";
+    public static final String TIS_NO_EVENTS = "b/180915942";
+    public static final String GET_RECENTS_FAILED = "b/177472267";
 }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 65df614..6f1b2f9 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -67,8 +67,9 @@
 
     /**
      * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this.
+     * TODO: Remove the atomic animation altogether and just go to OVERVIEW directly (b/175137718).
      */
-    public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f;
+    public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 1f;
     protected final long ATOMIC_DURATION = getAtomicDuration();
 
     protected final Launcher mLauncher;
@@ -201,10 +202,6 @@
 
         mFromState = newFromState;
         mToState = newToState;
-        if (TestProtocol.sDebugTracing) {
-            Log.d(TestProtocol.OVERIEW_NOT_ALLAPPS, "reinitCurrentAnimation: "
-                    + newToState.ordinal + " " + getClass().getSimpleName());
-        }
 
         mStartProgress = 0;
         mPassedOverviewAtomicThreshold = false;
@@ -342,9 +339,7 @@
         if (!goingBetweenNormalAndOverview(fromState, toState)) {
             return;
         }
-        float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD
-                : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD;
-        boolean passedThreshold = progress >= threshold;
+        boolean passedThreshold = progress >= ATOMIC_OVERVIEW_ANIM_THRESHOLD;
         if (passedThreshold != mPassedOverviewAtomicThreshold) {
             LauncherState atomicFromState = passedThreshold ? fromState: toState;
             LauncherState atomicToState = passedThreshold ? toState : fromState;
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 098d90d..61bd30a 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -41,7 +41,6 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.folder.Folder;
@@ -59,6 +58,7 @@
 import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.FloatingIconView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
 import com.android.launcher3.widget.WidgetManagerHelper;
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 7baeab8..f876dd9 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED;
 
 import android.view.View;
 import android.view.View.OnLongClickListener;
@@ -32,6 +33,7 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
+import com.android.launcher3.logging.StatsLogManager.StatsLogger;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
@@ -55,7 +57,7 @@
         if (!(v.getTag() instanceof ItemInfo)) return false;
 
         launcher.setWaitingForResult(null);
-        beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
+        beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
         return true;
     }
 
@@ -86,6 +88,12 @@
         if (!launcher.isInState(ALL_APPS) && !launcher.isInState(OVERVIEW)) return false;
         if (launcher.getWorkspace().isSwitchingState()) return false;
 
+        StatsLogger logger = launcher.getStatsLogManager().logger();
+        if (v.getTag() instanceof ItemInfo) {
+            logger.withItemInfo((ItemInfo) v.getTag());
+        }
+        logger.log(LAUNCHER_ALLAPPS_ITEM_LONG_PRESSED);
+
         // Start the drag
         final DragController dragController = launcher.getDragController();
         dragController.addDragListener(new DragController.DragListener() {
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index f05f15e..8a64f3d 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -147,13 +147,6 @@
     }
 
     @Override
-    public void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY) {
-        view.setTranslationX(defaultTranslationX);
-        view.setTranslationY(translation);
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollY();
     }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index b9acfa3..e1cec87 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -66,8 +66,6 @@
     int getSecondaryDimension(View view);
     FloatProperty<View> getPrimaryViewTranslate();
     FloatProperty<View> getSecondaryViewTranslate();
-    void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY);
     int getPrimaryScroll(View view);
     float getPrimaryScale(View view);
     int getChildStart(View view);
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 3663b5f..bcaf5f4 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -143,13 +143,6 @@
     }
 
     @Override
-    public void setPrimaryAndResetSecondaryTranslate(
-            View view, float translation, float defaultTranslationX, float defaultTranslationY) {
-        view.setTranslationX(translation);
-        view.setTranslationY(defaultTranslationY);
-    }
-
-    @Override
     public int getPrimaryScroll(View view) {
         return view.getScrollX();
     }
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 804fb3e..ae34257 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -34,6 +34,7 @@
 import android.view.ViewConfiguration;
 import android.widget.TextView;
 
+import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.BaseRecyclerView;
@@ -99,6 +100,7 @@
     private boolean mIsThumbDetached;
     private final boolean mCanThumbDetach;
     private boolean mIgnoreDragGesture;
+    private boolean mIsRecyclerViewFirstChildInParent = true;
 
     // This is the offset from the top of the scrollbar when the user first starts touching.  To
     // prevent jumping, this offset is applied as the user scrolls.
@@ -112,6 +114,7 @@
 
     protected BaseRecyclerView mRv;
     private RecyclerView.OnScrollListener mOnScrollListener;
+    @Nullable private OnFastScrollChangeListener mOnFastScrollChangeListener;
 
     private int mDownX;
     private int mDownY;
@@ -188,6 +191,9 @@
         updatePopupY(y);
         mThumbOffsetY = y;
         invalidate();
+        if (mOnFastScrollChangeListener != null) {
+            mOnFastScrollChangeListener.onThumbOffsetYChanged(mThumbOffsetY);
+        }
     }
 
     public int getThumbOffsetY() {
@@ -391,7 +397,9 @@
             return false;
         }
         getHitRect(sTempRect);
-        sTempRect.top += mRv.getScrollBarTop();
+        if (mIsRecyclerViewFirstChildInParent) {
+            sTempRect.top += mRv.getScrollBarTop();
+        }
         if (outOffset != null) {
             outOffset.set(sTempRect.left, sTempRect.top);
         }
@@ -404,4 +412,23 @@
         // alpha is so low, it does not matter.
         return false;
     }
+
+    public void setIsRecyclerViewFirstChildInParent(boolean isRecyclerViewFirstChildInParent) {
+        mIsRecyclerViewFirstChildInParent = isRecyclerViewFirstChildInParent;
+    }
+
+    public void setOnFastScrollChangeListener(
+            @Nullable OnFastScrollChangeListener onFastScrollChangeListener) {
+        mOnFastScrollChangeListener = onFastScrollChangeListener;
+    }
+
+    /**
+     * A callback that is invoked when there is a scroll change in {@link RecyclerViewFastScroller}.
+     */
+    public interface OnFastScrollChangeListener {
+        /**
+         * Called when the thumb offset vertical position, in pixels, has changed to {@code y}.
+         */
+        void onThumbOffsetYChanged(int y);
+    }
 }
diff --git a/src/com/android/launcher3/views/WorkEduView.java b/src/com/android/launcher3/views/WorkEduView.java
index 03d3026..c8cf627 100644
--- a/src/com/android/launcher3/views/WorkEduView.java
+++ b/src/com/android/launcher3/views/WorkEduView.java
@@ -168,8 +168,8 @@
     }
 
     private AllAppsPagedView getAllAppsPagedView() {
-        View v =  mLauncher.getAppsView().getContentView();
-        return  (v instanceof AllAppsPagedView)  ? (AllAppsPagedView) v : null;
+        View v = mLauncher.getAppsView().getContentView();
+        return (v instanceof AllAppsPagedView) ? (AllAppsPagedView) v : null;
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
similarity index 96%
rename from src/com/android/launcher3/LauncherAppWidgetHost.java
rename to src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index fea26df..d745754 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import static android.app.Activity.RESULT_CANCELED;
 
@@ -29,12 +29,13 @@
 import android.util.SparseArray;
 import android.widget.Toast;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.TestProtocol;
-import com.android.launcher3.widget.DeferredAppWidgetHostView;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.ArrayList;
@@ -199,7 +200,7 @@
         }
     }
 
-    void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
+    public void addPendingView(int appWidgetId, PendingAppWidgetHostView view) {
         mPendingViews.put(appWidgetId, view);
     }
 
@@ -247,7 +248,7 @@
         super.onProviderChanged(appWidgetId, info);
         // The super method updates the dimensions of the providerInfo. Update the
         // launcher spans accordingly.
-        info.initSpans(mContext);
+        info.initSpans(mContext, LauncherAppState.getIDP(mContext));
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 41098f9..3285c18 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -16,12 +16,9 @@
 
 package com.android.launcher3.widget;
 
-import static com.android.launcher3.Utilities.ATLEAST_S;
-
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.graphics.PointF;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
@@ -37,7 +34,6 @@
 
 import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -220,16 +216,6 @@
     }
 
     @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-
-        if (ATLEAST_S) {
-            float density = getContext().getResources().getDisplayMetrics().density;
-            setCurrentSize(new PointF(w / density, h / density));
-        }
-    }
-
-    @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         info.setClassName(getClass().getName());
diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
similarity index 86%
rename from src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
rename to src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
index 618b5de..ce97d2e 100644
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetProviderInfo.java
@@ -1,4 +1,4 @@
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
@@ -11,6 +11,9 @@
 import android.os.Parcel;
 import android.os.UserHandle;
 
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.ComponentWithLabelAndIcon;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -48,7 +51,7 @@
             launcherInfo = new LauncherAppWidgetProviderInfo(p);
             p.recycle();
         }
-        launcherInfo.initSpans(context);
+        launcherInfo.initSpans(context, LauncherAppState.getIDP(context));
         return launcherInfo;
     }
 
@@ -58,9 +61,7 @@
         super(in);
     }
 
-    public void initSpans(Context context) {
-        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
-
+    public void initSpans(Context context, InvariantDeviceProfile idp) {
         Point landCellSize = idp.landscapeProfile.getCellSize();
         Point portCellSize = idp.portraitProfile.getCellSize();
 
@@ -71,8 +72,12 @@
 
         // We want to account for the extra amount of padding that we are adding to the widget
         // to ensure that it gets the full amount of space that it has requested.
-        Rect widgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(
-                context, provider, null);
+        // If grids supports insetting widgets, we do not account for widget padding.
+        Rect widgetPadding = new Rect();
+        if (!idp.landscapeProfile.shouldInsetWidgets()
+                || !idp.portraitProfile.shouldInsetWidgets()) {
+            AppWidgetHostView.getDefaultPaddingForWidget(context, provider, widgetPadding);
+        }
         spanX = Math.max(1, (int) Math.ceil(
                         (minWidth + widgetPadding.left + widgetPadding.right) / smallestCellWidth));
         spanY = Math.max(1, (int) Math.ceil(
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index ed42bc4..6163b51 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.widget;
 
 import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetProviderInfo;
 import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -25,8 +26,11 @@
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 
+import com.android.launcher3.BaseActivity;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Reorderable;
 import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.views.ActivityContext;
 
 import java.util.ArrayList;
 
@@ -50,11 +54,16 @@
     private final PointF mTranslationForReorderPreview = new PointF(0, 0);
     private float mScaleForReorderBounce = 1f;
 
+    private final Rect mTempRect = new Rect();
+
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mChildrenFocused;
 
+    protected final BaseActivity mActivity;
+
     public NavigableAppWidgetHostView(Context context) {
         super(context);
+        mActivity = ActivityContext.lookupContext(context);
     }
 
     @Override
@@ -222,6 +231,25 @@
         int width = (int) (getMeasuredWidth() * mScaleToFit);
         int height = (int) (getMeasuredHeight() * mScaleToFit);
 
-        bounds.set(0, 0 , width, height);
+        getWidgetInset(mActivity.getDeviceProfile(), mTempRect);
+        bounds.set(mTempRect.left, mTempRect.top, width - mTempRect.right,
+                height - mTempRect.bottom);
+    }
+
+    /**
+     * Widgets have padding added by the system. We may choose to inset this padding if the grid
+     * supports it.
+     */
+    public void getWidgetInset(DeviceProfile grid, Rect out) {
+        if (!grid.shouldInsetWidgets()) {
+            out.setEmpty();
+            return;
+        }
+        AppWidgetProviderInfo info = getAppWidgetInfo();
+        if (info == null) {
+            out.set(grid.inv.defaultWidgetPadding);
+        } else {
+            AppWidgetHostView.getDefaultPaddingForWidget(getContext(), info.provider, out);
+        }
     }
 }
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index bef9a08..ee0b84e 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -20,7 +20,6 @@
 import android.appwidget.AppWidgetHostView;
 import android.os.Bundle;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.PendingAddItemInfo;
 
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index 8c3206d..4b113d8 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -22,7 +22,6 @@
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.PointF;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
@@ -30,6 +29,7 @@
 import android.text.Layout;
 import android.text.StaticLayout;
 import android.text.TextPaint;
+import android.util.SizeF;
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 import android.view.View;
@@ -112,7 +112,7 @@
     }
 
     @Override
-    public void updateAppWidgetSize(Bundle newOptions, List<PointF> sizes) {
+    public void updateAppWidgetSize(Bundle newOptions, List<SizeF> sizes) {
         // No-op
     }
 
diff --git a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
index ebc2a25..1ac5a33 100644
--- a/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
+++ b/src/com/android/launcher3/widget/WidgetAddFlowHandler.java
@@ -21,7 +21,6 @@
 import android.os.Parcelable;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.PendingRequestArgs;
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index bef91d2..8c315fd 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.widget;
 
+import static com.android.launcher3.Utilities.ATLEAST_S;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.CancellationSignal;
@@ -24,7 +26,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.LinearLayout;
@@ -60,12 +61,15 @@
     /** Widget preview width is calculated by multiplying this factor to the widget cell width. */
     private static final float PREVIEW_SCALE = 0.8f;
 
+    private int mPreviewWidth;
+    private int mPreviewHeight;
     protected int mPresetPreviewSize;
     private int mCellSize;
 
     private WidgetImageView mWidgetImage;
     private TextView mWidgetName;
     private TextView mWidgetDims;
+    private TextView mWidgetDescription;
 
     protected WidgetItem mItem;
 
@@ -106,15 +110,17 @@
     private void setContainerWidth() {
         mCellSize = (int) (mDeviceProfile.allAppsIconSizePx * WIDTH_SCALE);
         mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
+        mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mWidgetImage = (WidgetImageView) findViewById(R.id.widget_preview);
-        mWidgetName = ((TextView) findViewById(R.id.widget_name));
-        mWidgetDims = ((TextView) findViewById(R.id.widget_dims));
+        mWidgetImage = findViewById(R.id.widget_preview);
+        mWidgetName = findViewById(R.id.widget_name);
+        mWidgetDims = findViewById(R.id.widget_dims);
+        mWidgetDescription = findViewById(R.id.widget_description);
     }
 
     /**
@@ -128,6 +134,9 @@
         mWidgetImage.setBitmap(null, null);
         mWidgetName.setText(null);
         mWidgetDims.setText(null);
+        mWidgetDescription.setText(null);
+        mWidgetDescription.setVisibility(GONE);
+        mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
 
         if (mActiveRequest != null) {
             mActiveRequest.cancel();
@@ -142,8 +151,17 @@
                 mItem.spanX, mItem.spanY));
         mWidgetDims.setContentDescription(getContext().getString(
                 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
-        mWidgetPreviewLoader = loader;
+        if (ATLEAST_S && mItem.widgetInfo != null) {
+            CharSequence description = mItem.widgetInfo.loadDescription(getContext());
+            if (description != null && description.length() > 0) {
+                mWidgetDescription.setText(description);
+                mWidgetDescription.setVisibility(VISIBLE);
+            } else {
+                mWidgetDescription.setVisibility(GONE);
+            }
+        }
 
+        mWidgetPreviewLoader = loader;
         if (item.activityInfo != null) {
             setTag(new PendingAddShortcutInfo(item.activityInfo));
         } else {
@@ -181,6 +199,11 @@
             return;
         }
         if (bitmap != null) {
+            LayoutParams layoutParams = (LayoutParams) mWidgetImage.getLayoutParams();
+            layoutParams.width = bitmap.getWidth();
+            layoutParams.height = bitmap.getHeight();
+            mWidgetImage.setLayoutParams(layoutParams);
+
             mWidgetImage.setBitmap(bitmap, mWidgetPreviewLoader.getBadgeForUser(mItem.user,
                     BaseIconFactory.getBadgeSizeForIconSize(mDeviceProfile.allAppsIconSizePx)));
             if (mAnimatePreview) {
@@ -197,8 +220,16 @@
         if (mActiveRequest != null) {
             return;
         }
-        mActiveRequest = mWidgetPreviewLoader.getPreview(
-                mItem, mPresetPreviewSize, mPresetPreviewSize, this);
+        mActiveRequest = mWidgetPreviewLoader.getPreview(mItem, mPreviewWidth, mPreviewHeight,
+                this);
+    }
+
+    /** Sets the widget preview image size in number of cells. */
+    public void setPreviewSize(int spanX, int spanY) {
+        int padding = 2 * getResources()
+                .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
+        mPreviewWidth = mDeviceProfile.cellWidthPx * spanX + padding;
+        mPreviewHeight = mDeviceProfile.cellHeightPx * spanY + padding;
     }
 
     @Override
@@ -233,12 +264,6 @@
     }
 
     @Override
-    public void setLayoutParams(ViewGroup.LayoutParams params) {
-        params.width = params.height = mCellSize;
-        super.setLayoutParams(params);
-    }
-
-    @Override
     public CharSequence getAccessibilityClassName() {
         return WidgetCell.class.getName();
     }
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index 2438bdf..12e0d43 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -3,23 +3,23 @@
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
 import android.content.Context;
-import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.Log;
+import android.util.SizeF;
 import android.view.View;
 
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
+import java.util.stream.Collectors;
 
 public class WidgetHostViewLoader implements DragController.DragListener {
     private static final String TAG = "WidgetHostViewLoader";
@@ -155,7 +155,7 @@
     }
 
     public static Bundle getDefaultOptionsForWidget(Context context, PendingAddWidgetInfo info) {
-        ArrayList<PointF> sizes = AppWidgetResizeFrame
+        ArrayList<SizeF> sizes = AppWidgetResizeFrame
                 .getWidgetSizes(context, info.spanX, info.spanY);
 
         Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context,
@@ -164,19 +164,19 @@
         float xPaddingDips = (padding.left + padding.right) / density;
         float yPaddingDips = (padding.top + padding.bottom) / density;
 
-        for (PointF size : sizes) {
-            size.x = Math.max(0.f, size.x - xPaddingDips);
-            size.y = Math.max(0.f, size.y - yPaddingDips);
-        }
+        ArrayList<SizeF> paddedSizes = sizes.stream().map(
+                size -> new SizeF(Math.max(0.f, size.getWidth() - xPaddingDips),
+                        Math.max(0.f, size.getHeight() - yPaddingDips))).collect(
+                Collectors.toCollection(ArrayList::new));
 
-        Rect rect = AppWidgetResizeFrame.getMinMaxSizes(sizes, null /* outRect */);
+        Rect rect = AppWidgetResizeFrame.getMinMaxSizes(paddedSizes, null /* outRect */);
 
         Bundle options = new Bundle();
         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, rect.left);
         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, rect.top);
         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, rect.right);
         options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, rect.bottom);
-        options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizes);
+        options.putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, paddedSizes);
         return options;
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetManagerHelper.java b/src/com/android/launcher3/widget/WidgetManagerHelper.java
index c0c5c48..15fa844 100644
--- a/src/com/android/launcher3/widget/WidgetManagerHelper.java
+++ b/src/com/android/launcher3/widget/WidgetManagerHelper.java
@@ -27,18 +27,14 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -122,15 +118,6 @@
                 appWidgetId).getBoolean(WIDGET_OPTION_RESTORE_COMPLETED);
     }
 
-    public static Map<ComponentKey, AppWidgetProviderInfo> getAllProvidersMap(Context context) {
-        if (WidgetsModel.GO_DISABLE_WIDGETS) {
-            return Collections.emptyMap();
-        }
-        return allWidgetsSteam(context).collect(
-                        Collectors.toMap(info -> new ComponentKey(info.provider, info.getProfile()),
-                        Function.identity()));
-    }
-
     private static Stream<AppWidgetProviderInfo> allWidgetsSteam(Context context) {
         AppWidgetManager awm = context.getSystemService(AppWidgetManager.class);
         return Stream.concat(
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 223cda2..6abbf21 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -24,22 +24,25 @@
 import android.util.AttributeSet;
 import android.util.IntProperty;
 import android.util.Pair;
-import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Interpolator;
+import android.widget.ScrollView;
+import android.widget.TableLayout;
+import android.widget.TableRow;
 import android.widget.TextView;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
-import com.android.launcher3.ResourceUtils;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.dragndrop.LivePreviewWidgetCell;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.widget.util.WidgetsTableUtils;
 
 import java.util.List;
 
@@ -65,6 +68,8 @@
     private static final int DEFAULT_CLOSE_DURATION = 200;
     private ItemInfo mOriginalItemInfo;
     private Rect mInsets;
+    private final int mMaxTableHeight;
+    private int mMaxHorizontalSpan = 4;
 
     public WidgetsBottomSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
@@ -75,12 +80,41 @@
         setWillNotDraw(false);
         mInsets = new Rect();
         mContent = this;
+        DeviceProfile deviceProfile = mLauncher.getDeviceProfile();
+        // Set the max table height to 2 / 3 of the grid height so that the bottom picker won't
+        // take over the entire view vertically.
+        mMaxTableHeight = deviceProfile.inv.numRows * 2 / 3  * deviceProfile.cellHeightPx;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int paddingPx = 2 * getResources().getDimensionPixelOffset(
+                R.dimen.widget_cell_horizontal_padding);
+        int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth()
+                / (mLauncher.getDeviceProfile().cellWidthPx + paddingPx);
+
+        if (mMaxHorizontalSpan != maxHorizontalSpan) {
+            // Ensure the table layout is showing widgets in the right column after measure.
+            mMaxHorizontalSpan = maxHorizontalSpan;
+            onWidgetsBound();
+        }
     }
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         super.onLayout(changed, l, t, r, b);
         setTranslationShift(mTranslationShift);
+
+        // Ensure the scroll view height is not larger than mMaxTableHeight, which is a value
+        // smaller than the entire screen height.
+        ScrollView widgetsTableScrollView = findViewById(R.id.widgets_table_scroll_view);
+        if (widgetsTableScrollView.getMeasuredHeight() > mMaxTableHeight) {
+            ViewGroup.LayoutParams layoutParams = widgetsTableScrollView.getLayoutParams();
+            layoutParams.height = mMaxTableHeight;
+            widgetsTableScrollView.setLayoutParams(layoutParams);
+        }
     }
 
     public void populateAndShow(ItemInfo itemInfo) {
@@ -101,39 +135,21 @@
                         mOriginalItemInfo.getTargetComponent().getPackageName(),
                         mOriginalItemInfo.user));
 
-        ViewGroup widgetRow = findViewById(R.id.widgets);
-        ViewGroup widgetCells = widgetRow.findViewById(R.id.widgets_cell_list);
+        TableLayout widgetsTable = findViewById(R.id.widgets_table);
+        widgetsTable.removeAllViews();
 
-        widgetCells.removeAllViews();
-
-        for (int i = 0; i < widgets.size(); i++) {
-            WidgetCell widget = addItemCell(widgetCells);
-            widget.applyFromCellItem(widgets.get(i), LauncherAppState.getInstance(mLauncher)
-                    .getWidgetCache());
-            widget.ensurePreview();
-            widget.setVisibility(View.VISIBLE);
-            if (i < widgets.size() - 1) {
-                addDivider(widgetCells);
-            }
-        }
-
-        if (widgets.size() == 1) {
-            // If there is only one widget, we want to center it instead of left-align.
-            WidgetsBottomSheet.LayoutParams params = (WidgetsBottomSheet.LayoutParams)
-                    widgetRow.getLayoutParams();
-            params.gravity = Gravity.CENTER_HORIZONTAL;
-        } else {
-            // Otherwise, add an empty view to the start as padding (but still scroll edge to edge).
-            View leftPaddingView = LayoutInflater.from(getContext()).inflate(
-                    R.layout.widget_list_divider, widgetRow, false);
-            leftPaddingView.getLayoutParams().width = ResourceUtils.pxFromDp(
-                    16, getResources().getDisplayMetrics());
-            widgetCells.addView(leftPaddingView, 0);
-        }
-    }
-
-    private void addDivider(ViewGroup parent) {
-        LayoutInflater.from(getContext()).inflate(R.layout.widget_list_divider, parent, true);
+        WidgetsTableUtils.groupWidgetItemsIntoTable(widgets, mMaxHorizontalSpan).forEach(row -> {
+            TableRow tableRow = new TableRow(getContext());
+            row.forEach(widgetItem -> {
+                WidgetCell widget = addItemCell(tableRow);
+                widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
+                widget.applyFromCellItem(widgetItem, LauncherAppState.getInstance(mLauncher)
+                        .getWidgetCache());
+                widget.ensurePreview();
+                widget.setVisibility(View.VISIBLE);
+            });
+            widgetsTable.addView(tableRow);
+        });
     }
 
     protected WidgetCell addItemCell(ViewGroup parent) {
diff --git a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
index 1086987..8b3bbce 100644
--- a/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/widget/custom/CustomAppWidgetProviderInfo.java
@@ -22,8 +22,9 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 /**
  * Custom app widget provider info that can be used as a widget, but provide extra functionality
@@ -57,7 +58,7 @@
     }
 
     @Override
-    public void initSpans(Context context) { }
+    public void initSpans(Context context, InvariantDeviceProfile idp) { }
 
     @Override
     public String getLabel(PackageManager packageManager) {
diff --git a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
index 0b66ec0..329a444 100644
--- a/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
+++ b/src/com/android/launcher3/widget/custom/CustomWidgetManager.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3.widget.custom;
 
-import static com.android.launcher3.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.CLS_CUSTOM_WIDGET_PREFIX;
 
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -29,12 +29,12 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.systemui.plugins.CustomWidgetPlugin;
 import com.android.systemui.plugins.PluginListener;
 
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
new file mode 100644
index 0000000..95fa05f
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 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.widget.picker;
+
+import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.views.RecyclerViewFastScroller;
+import com.android.launcher3.widget.picker.WidgetsFullSheet.SearchAndRecommendationViewHolder;
+import com.android.launcher3.workprofile.PersonalWorkPagedView;
+
+/**
+ * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and
+ * vertical displacement upon scrolling.
+ */
+final class SearchAndRecommendationsScrollController implements
+        RecyclerViewFastScroller.OnFastScrollChangeListener {
+    private final boolean mHasWorkProfile;
+    private final SearchAndRecommendationViewHolder mViewHolder;
+    private final WidgetsRecyclerView mPrimaryRecyclerView;
+
+    // The following are only non null if mHasWorkProfile is true.
+    @Nullable private final WidgetsRecyclerView mWorkRecyclerView;
+    @Nullable private final View mPrimaryWorkTabsView;
+    @Nullable private final PersonalWorkPagedView mPrimaryWorkViewPager;
+
+    private WidgetsRecyclerView mCurrentRecyclerView;
+    private int mMaxCollapsibleHeight = 0;
+
+    SearchAndRecommendationsScrollController(
+            boolean hasWorkProfile,
+            SearchAndRecommendationViewHolder viewHolder,
+            WidgetsRecyclerView primaryRecyclerView,
+            @Nullable WidgetsRecyclerView workRecyclerView,
+            @Nullable View personalWorkTabsView,
+            @Nullable PersonalWorkPagedView primaryWorkViewPager) {
+        mHasWorkProfile = hasWorkProfile;
+        mViewHolder = viewHolder;
+        mPrimaryRecyclerView = primaryRecyclerView;
+        mWorkRecyclerView = workRecyclerView;
+        mPrimaryWorkTabsView = personalWorkTabsView;
+        mPrimaryWorkViewPager = primaryWorkViewPager;
+        mCurrentRecyclerView = mPrimaryRecyclerView;
+    }
+
+    /** Sets the current active {@link WidgetsRecyclerView}. */
+    public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
+        mCurrentRecyclerView = currentRecyclerView;
+    }
+
+    /**
+     * Updates the margin and padding of {@link WidgetsFullSheet} to accumulate collapsible views.
+     */
+    public void updateMarginAndPadding() {
+        // The maximum vertical distance, in pixels, until the last collapsible element is not
+        // visible from the screen when the user scrolls down the recycler view.
+        mMaxCollapsibleHeight = mViewHolder.mContainer.getPaddingTop()
+                + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
+                + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
+
+        int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
+        if (mHasWorkProfile) {
+            // In a work profile setup, the full widget sheet contains the following views:
+            //           -------               -|
+            //           Widgets               -|---> LinearLayout for search & recommendations
+            //          Search bar             -|
+            //      Personal | Work
+            //           View Pager
+            //
+            // Views after the search & recommendations are not bound by RelativelyLayout param.
+            // To position them on the expected location, padding & margin are added to these views
+
+            // Tabs should have a padding of the height of the search & recommendations container.
+            mPrimaryWorkTabsView.setPadding(
+                    mPrimaryWorkTabsView.getPaddingLeft(),
+                    topContainerHeight,
+                    mPrimaryWorkTabsView.getPaddingRight(),
+                    mPrimaryWorkTabsView.getPaddingBottom());
+
+            // Instead of setting the top offset directly, we split the top offset into two values:
+            // 1. topOffsetAfterAllViewsCollapsed: this is the top offset after all collapsible
+            //    views are no longer visible on the screen.
+            //    This value is set as the margin for the view pager.
+            // 2. mMaxCollapsibleDistance
+            //    This value is set as the padding for the recycler views in order to work with
+            //    clipToPadding="false", which is an attribute for not showing top / bottom padding
+            //    when a recycler view has not reached the top or bottom of the list.
+            //    e.g. a list of 10 entries, only 3 entries are visible at a time.
+            //         case 1: recycler view is scrolled to the top. Top padding is visible/
+            //         (top padding)
+            //         item 1
+            //         item 2
+            //         item 3
+            //
+            //         case 2: recycler view is scrolled to the middle. No padding is visible.
+            //         item 4
+            //         item 5
+            //         item 6
+            //
+            //         case 3: recycler view is scrolled to the end. bottom padding is visible.
+            //         item 8
+            //         item 9
+            //         item 10
+            //         (bottom padding): not set in this case.
+            //
+            // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and
+            // mMaxCollapsibleDistance should equal to the top container height.
+            int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView)
+                    - mPrimaryWorkTabsView.getPaddingTop();
+            int topOffsetAfterAllViewsCollapsed =
+                    topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight;
+
+            RelativeLayout.LayoutParams layoutParams =
+                    (RelativeLayout.LayoutParams) mPrimaryWorkViewPager.getLayoutParams();
+            layoutParams.setMargins(0, topOffsetAfterAllViewsCollapsed, 0, 0);
+            mPrimaryWorkViewPager.setLayoutParams(layoutParams);
+            mPrimaryWorkViewPager.requestLayout();
+
+            mPrimaryRecyclerView.setPadding(
+                    mPrimaryRecyclerView.getPaddingLeft(),
+                    mMaxCollapsibleHeight,
+                    mPrimaryRecyclerView.getPaddingRight(),
+                    mPrimaryRecyclerView.getPaddingBottom());
+            mWorkRecyclerView.setPadding(
+                    mWorkRecyclerView.getPaddingLeft(),
+                    mMaxCollapsibleHeight,
+                    mWorkRecyclerView.getPaddingRight(),
+                    mWorkRecyclerView.getPaddingBottom());
+        } else {
+            mPrimaryRecyclerView.setPadding(
+                    mPrimaryRecyclerView.getPaddingLeft(),
+                    topContainerHeight,
+                    mPrimaryRecyclerView.getPaddingRight(),
+                    mPrimaryRecyclerView.getPaddingBottom());
+        }
+    }
+
+    /**
+     * Changes the displacement of collapsible views (e.g. title & widget recommendations) and fixed
+     * views (e.g. recycler views, tabs) upon scrolling.
+     */
+    @Override
+    public void onThumbOffsetYChanged(int unused) {
+        // Always use the recycler view offset because fast scroller offset has a different scale.
+        int recyclerViewYOffset = mCurrentRecyclerView.getCurrentScrollY();
+        if (recyclerViewYOffset < 0) return;
+        if (mMaxCollapsibleHeight > 0) {
+            int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight);
+            mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
+            mViewHolder.mSearchBar.setTranslationY(yDisplacement);
+            if (mHasWorkProfile) {
+                mPrimaryWorkTabsView.setTranslationY(yDisplacement);
+            }
+        }
+    }
+
+    /** Resets any previous view translation. */
+    public void reset() {
+        mViewHolder.mHeaderTitle.setTranslationY(0);
+        mViewHolder.mSearchBar.setTranslationY(0);
+        if (mHasWorkProfile) {
+            mPrimaryWorkTabsView.setTranslationY(0);
+        }
+    }
+
+    /** private the height, in pixel, + the vertical margins of a given view. */
+    private static int measureHeightWithVerticalMargins(View view) {
+        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+        return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+                + marginLayoutParams.topMargin;
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 39953b1..52a2fc5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -34,6 +34,8 @@
 import android.view.View;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+import android.widget.EditText;
+import android.widget.TextView;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
@@ -43,13 +45,13 @@
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 import com.android.launcher3.widget.BaseWidgetSheet;
+import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
@@ -61,7 +63,8 @@
  * Popup for showing the full list of available widgets
  */
 public class WidgetsFullSheet extends BaseWidgetSheet
-        implements Insettable, ProviderChangedListener, OnActivePageChangedListener {
+        implements Insettable, ProviderChangedListener, OnActivePageChangedListener,
+        WidgetsRecyclerView.HeaderViewDimensionsProvider {
 
     private static final long DEFAULT_OPEN_DURATION = 267;
     private static final long FADE_IN_DURATION = 150;
@@ -77,6 +80,10 @@
             mPrimaryWidgetsFilter.negate();
 
     @Nullable private PersonalWorkPagedView mViewPager;
+    private int mInitialTabsHeight = 0;
+    private View mTabsView;
+    private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
+    private SearchAndRecommendationsScrollController mSearchAndRecommendationsScrollController;
 
     public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -98,8 +105,9 @@
         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
         int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
                 : R.layout.widgets_full_sheet_recyclerview;
-        layoutInflater.inflate(contentLayoutRes,  springLayout, true);
+        layoutInflater.inflate(contentLayoutRes, springLayout, true);
 
+        RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller);
         if (mHasWorkProfile) {
             mViewPager = findViewById(R.id.widgets_view_pager);
             // Temporarily disable swipe gesture until widgets list horizontal scrollviews per
@@ -108,10 +116,12 @@
             mViewPager.initParentViews(this);
             mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
             mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
+            mTabsView = findViewById(R.id.tabs);
             findViewById(R.id.tab_personal)
                     .setOnClickListener((View view) -> mViewPager.snapToPage(0));
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> mViewPager.snapToPage(1));
+            fastScroller.setIsRecyclerViewFirstChildInParent(false);
             springLayout.addSpringView(R.id.primary_widgets_list_view);
             springLayout.addSpringView(R.id.work_widgets_list_view);
         } else {
@@ -119,12 +129,40 @@
             springLayout.addSpringView(R.id.primary_widgets_list_view);
         }
 
+        layoutInflater.inflate(R.layout.widgets_full_sheet_search_and_recommendations, springLayout,
+                true);
+        springLayout.addSpringView(R.id.search_and_recommendations_container);
+
+        mSearchAndRecommendationViewHolder = new SearchAndRecommendationViewHolder(
+                findViewById(R.id.search_and_recommendations_container));
+        mSearchAndRecommendationsScrollController = new SearchAndRecommendationsScrollController(
+                mHasWorkProfile,
+                mSearchAndRecommendationViewHolder,
+                findViewById(R.id.primary_widgets_list_view),
+                mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null,
+                mTabsView,
+                mViewPager);
+        fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController);
+
         onWidgetsBound();
     }
 
     @Override
     public void onActivePageChanged(int currentActivePage) {
-        mAdapters.get(currentActivePage).mWidgetsRecyclerView.bindFastScrollbar();
+        WidgetsRecyclerView currentRecyclerView =
+                mAdapters.get(currentActivePage).mWidgetsRecyclerView;
+        currentRecyclerView.bindFastScrollbar();
+        mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView);
+
+        reset();
+    }
+
+    private void reset() {
+        mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.scrollToTop();
+        if (mHasWorkProfile) {
+            mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
+        }
+        mSearchAndRecommendationsScrollController.reset();
     }
 
     @VisibleForTesting
@@ -198,14 +236,21 @@
         setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
                 MeasureSpec.getSize(heightMeasureSpec));
 
-        int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx
-                + deviceProfile.workspaceCellPaddingXPx);
+        int paddingPx = 2 * getResources().getDimensionPixelOffset(
+                R.dimen.widget_cell_horizontal_padding);
+        int maxSpansPerRow = getMeasuredWidth() / (deviceProfile.cellWidthPx + paddingPx);
         mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
                 maxSpansPerRow);
         if (mHasWorkProfile) {
             mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
                     maxSpansPerRow);
         }
+
+        if (mInitialTabsHeight == 0 && mTabsView != null) {
+            mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView);
+        }
+
+        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
     }
 
     @Override
@@ -325,6 +370,21 @@
         AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
     }
 
+    @Override
+    public int getHeaderViewHeight() {
+        // No need to check work profile here because mInitialTabHeight is always 0 if there is no
+        // work profile.
+        return mInitialTabsHeight
+                + measureHeightWithVerticalMargins(mSearchAndRecommendationViewHolder.mContainer);
+    }
+
+    /** private the height, in pixel, + the vertical margins of a given view. */
+    private static int measureHeightWithVerticalMargins(View view) {
+        MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
+        return view.getMeasuredHeight() + marginLayoutParams.bottomMargin
+                + marginLayoutParams.topMargin;
+    }
+
     /** A holder class for holding adapters & their corresponding recycler view. */
     private final class AdapterHolder {
         static final int PRIMARY = 0;
@@ -354,9 +414,24 @@
         void setup(WidgetsRecyclerView recyclerView) {
             mWidgetsRecyclerView = recyclerView;
             mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
+            mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
             mWidgetsRecyclerView.setEdgeEffectFactory(
                     ((TopRoundedCornerView) mContent).createEdgeEffectFactory());
             mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
         }
     }
+
+    final class SearchAndRecommendationViewHolder {
+        final View mContainer;
+        final View mCollapseHandle;
+        final EditText mSearchBar;
+        final TextView mHeaderTitle;
+
+        SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) {
+            mContainer = searchAndRecommendationContainer;
+            mCollapseHandle = mContainer.findViewById(R.id.collapse_handle);
+            mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
+            mHeaderTitle = mContainer.findViewById(R.id.title);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index 823fb7b..070a9aa 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -157,14 +157,29 @@
     private void setTitles(WidgetsListHeaderEntry entry) {
         mTitle.setText(entry.mPkgItem.title);
 
-        if (entry.widgetsCount > 0) {
-            Resources resources = getContext().getResources();
-            mSubtitle.setText(resources.getQuantityString(R.plurals.widgets_tray_subtitle,
-                    entry.widgetsCount, entry.widgetsCount));
-            mSubtitle.setVisibility(VISIBLE);
-        } else {
+        Resources resources = getContext().getResources();
+        if (entry.widgetsCount == 0 && entry.shortcutsCount == 0) {
             mSubtitle.setVisibility(GONE);
+            return;
         }
+
+        String subtitle;
+        if (entry.widgetsCount > 0 && entry.shortcutsCount > 0) {
+            String widgetsCount = resources.getQuantityString(R.plurals.widgets_count,
+                    entry.widgetsCount, entry.widgetsCount);
+            String shortcutsCount = resources.getQuantityString(R.plurals.shortcuts_count,
+                    entry.shortcutsCount, entry.shortcutsCount);
+            subtitle = resources.getString(R.string.widgets_and_shortcuts_count, widgetsCount,
+                    shortcutsCount);
+        } else if (entry.widgetsCount > 0) {
+            subtitle = resources.getQuantityString(R.plurals.widgets_count,
+                     entry.widgetsCount, entry.widgetsCount);
+        } else {
+            subtitle = resources.getQuantityString(R.plurals.shortcuts_count,
+                    entry.shortcutsCount, entry.shortcutsCount);
+        }
+        mSubtitle.setText(subtitle);
+        mSubtitle.setVisibility(VISIBLE);
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 2355700..9c8684a 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -112,7 +112,9 @@
                 TableRow row = (TableRow) table.getChildAt(i);
                 row.setVisibility(View.VISIBLE);
                 WidgetCell widget = (WidgetCell) row.getChildAt(j);
+                widget.clear();
                 WidgetItem widgetItem = widgetItemsPerRow.get(j);
+                widget.setPreviewSize(widgetItem.spanX, widgetItem.spanY);
                 widget.applyFromCellItem(widgetItem, mWidgetPreviewLoader);
                 widget.setApplyBitmapDeferred(mApplyBitmapDeferred);
                 widget.ensurePreview();
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index 52e9496..d65a809 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -40,6 +40,7 @@
 
     private final Point mFastScrollerOffset = new Point();
     private boolean mTouchDownOnScroller;
+    private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -135,8 +136,8 @@
     @Override
     protected int getAvailableScrollHeight() {
         View child = getChildAt(0);
-        return child.getMeasuredHeight() * mAdapter.getItemCount() - getScrollbarTrackHeight()
-                - mScrollbarTop;
+        return child.getMeasuredHeight() * mAdapter.getItemCount() + getScrollBarTop()
+                + getPaddingBottom() - mScrollbar.getHeight();
     }
 
     private boolean isModelNotReady() {
@@ -145,7 +146,9 @@
 
     @Override
     public int getScrollBarTop() {
-        return mScrollbarTop;
+        return mHeaderViewDimensionsProvider == null
+                ? mScrollbarTop
+                : mHeaderViewDimensionsProvider.getHeaderViewHeight() + mScrollbarTop;
     }
 
     @Override
@@ -171,4 +174,21 @@
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
     }
+
+    public void setHeaderViewDimensionsProvider(
+            HeaderViewDimensionsProvider headerViewDimensionsProvider) {
+        mHeaderViewDimensionsProvider = headerViewDimensionsProvider;
+    }
+
+    /**
+     * Provides dimensions of the header view that is shown at the top of a
+     * {@link WidgetsRecyclerView}.
+     */
+    public interface HeaderViewDimensionsProvider {
+        /**
+         * Returns the height, in pixels, of the header view that is shown at the top of a
+         * {@link WidgetsRecyclerView}.
+         */
+        int getHeaderViewHeight();
+    }
 }
diff --git a/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java b/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java
new file mode 100644
index 0000000..d12782c
--- /dev/null
+++ b/src/com/android/launcher3/widget/picker/search/WidgetsPickerSearchPipeline.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.widget.picker.search;
+
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * An interface for a pipeline to handle widgets search.
+ */
+public interface WidgetsPickerSearchPipeline {
+
+    /**
+     * Performs a search query asynchronically. Invokes {@code callback} when the search is
+     * complete.
+     */
+    void query(String input, Consumer<List<WidgetsListBaseEntry>> callback);
+
+    /**
+     * Cancels any ongoing search request.
+     */
+    default void cancel() {};
+
+    /**
+     * Cleans up after search is no longer needed.
+     */
+    default void destroy() {};
+}
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index a7e3472..a8c7c48 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -17,7 +17,6 @@
 import com.android.launcher3.AppFilter;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.config.FeatureFlags;
@@ -27,6 +26,7 @@
 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.model.WidgetsListContentEntry;
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index f243f27..744dee0 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -98,7 +98,7 @@
         <activity
             android:name="com.android.launcher3.testcomponent.TestLauncherActivity"
             android:clearTaskOnLaunch="true"
-            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
+            android:configChanges="keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenSize|screenLayout"
             android:enabled="false"
             android:label="Test launcher"
             android:launchMode="singleTask"
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index b3c1240..0edfbed 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -151,23 +151,30 @@
     }
 
     public static String dumpHprofData() {
-        if (sDumpWasGenerated) return "dump has already been generated by another test";
-        try {
-            final String fileName = getInstrumentation().getTargetContext().getFilesDir().getPath()
-                    + "/ActivityLeakHeapDump.hprof";
-            if (TestHelpers.isInLauncherProcess()) {
-                Debug.dumpHprofData(fileName);
-            } else {
-                final UiDevice device = UiDevice.getInstance(getInstrumentation());
-                device.executeShellCommand(
-                        "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+        String result;
+        if (sDumpWasGenerated) {
+            result = "dump has already been generated by another test";
+        } else {
+            try {
+                final String fileName =
+                        getInstrumentation().getTargetContext().getFilesDir().getPath()
+                                + "/ActivityLeakHeapDump.hprof";
+                if (TestHelpers.isInLauncherProcess()) {
+                    Debug.dumpHprofData(fileName);
+                } else {
+                    final UiDevice device = UiDevice.getInstance(getInstrumentation());
+                    device.executeShellCommand(
+                            "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
+                }
+                sDumpWasGenerated = true;
+                result = "memory dump filename: " + fileName;
+            } catch (Throwable e) {
+                Log.e(TAG, "dumpHprofData failed", e);
+                result = "failed to save memory dump";
             }
-            sDumpWasGenerated = true;
-            return "memory dump filename: " + fileName;
-        } catch (Throwable e) {
-            Log.e(TAG, "dumpHprofData failed", e);
-            return "failed to save memory dump";
         }
+        return result
+                + ". Full list of activities: " + ACTIVITY_LEAK_TRACKER.getActivitiesList();
     }
 
     protected AbstractLauncherUiTest() {
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
index dd216c7..2db7472 100644
--- a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.tapl.TestHelpers;
 
 import java.util.WeakHashMap;
+import java.util.stream.Collectors;
 
 public class ActivityLeakTracker implements Application.ActivityLifecycleCallbacks {
     private final WeakHashMap<Activity, Boolean> mActivities = new WeakHashMap<>();
@@ -81,4 +82,9 @@
 
         return mActivities.size() <= 2;
     }
+
+    public String getActivitiesList() {
+        return mActivities.keySet().stream().map(a -> a.getClass().getSimpleName())
+                .collect(Collectors.joining(","));
+    }
 }
diff --git a/tests/src/com/android/launcher3/ui/TestViewHelpers.java b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
index eceff34..083f580 100644
--- a/tests/src/com/android/launcher3/ui/TestViewHelpers.java
+++ b/tests/src/com/android/launcher3/ui/TestViewHelpers.java
@@ -24,9 +24,9 @@
 import android.view.View;
 import android.view.ViewGroup;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.testcomponent.AppWidgetNoConfig;
 import com.android.launcher3.testcomponent.AppWidgetWithConfig;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import java.util.concurrent.Callable;
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index aef26ae..63220ad 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -77,6 +78,17 @@
         mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
     }
 
+    @After
+    public void resumeAppStoreUpdate() {
+        executeOnLauncher(launcher -> {
+            if (launcher == null || launcher.getAppsView() == null) {
+                return;
+            }
+            launcher.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+            Log.d(TestProtocol.WORK_PROFILE_REMOVED, "resuming AppStore updates");
+        });
+    }
+
     @Test
     public void workTabExists() {
         mDevice.pressHome();
@@ -145,6 +157,12 @@
                 "work profile status (" + mProfileUserId + ") :"
                         + launcher.getAppsView().isWorkTabVisible()));
 
+
+        executeOnLauncher(launcher -> {
+            launcher.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
+            Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Defer all apps update");
+        });
+
         AtomicInteger attempt = new AtomicInteger(0);
         // verify work edu is seen next
         waitForLauncherCondition("Launcher did not show the next edu screen", l -> {
@@ -154,9 +172,11 @@
                 Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work tab not setup. Skipping test");
                 return false;
             }
-            return ((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
-                    == WORK_PAGE && ((TextView) workEduView.findViewById(
-                    R.id.content_text)).getText().equals(
+            if (((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
+                    != WORK_PAGE) {
+                Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work page not highlighted");
+            }
+            return ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
                     l.getResources().getString(R.string.work_profile_edu_work_apps));
         }, 60000);
     }
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 737f891..b421b0e 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,7 +28,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -39,6 +38,7 @@
 import com.android.launcher3.util.Wait;
 import com.android.launcher3.util.Wait.Condition;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Before;
 import org.junit.Rule;
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
index f146db5..714b11b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
@@ -23,12 +23,12 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 
 import org.junit.Rule;
 import org.junit.Test;
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
index fa495f5..9c6c317 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
@@ -37,7 +37,6 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -47,6 +46,7 @@
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TestViewHelpers;
 import com.android.launcher3.util.rule.ShellCommandRule;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
 import org.junit.After;
diff --git a/tests/src_common/com/android/launcher3/common/WidgetUtils.java b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
index c0913bf..ffad93f 100644
--- a/tests/src_common/com/android/launcher3/common/WidgetUtils.java
+++ b/tests/src_common/com/android/launcher3/common/WidgetUtils.java
@@ -23,12 +23,12 @@
 import android.content.Context;
 import android.os.Bundle;
 
-import com.android.launcher3.LauncherAppWidgetHost;
-import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
+import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 3fc83ff..c4a566b 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -67,7 +67,7 @@
                 () -> "Launching an app didn't open a new window: " + label);
 
         mLauncher.assertTrue(
-                "App didn't start: " + label,
+                "App didn't start: " + label + " (" + selector + ")",
                 TestHelpers.wait(Until.hasObject(selector), LauncherInstrumentation.WAIT_TIME_MS));
         return new Background(mLauncher);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 6afadfa..f279a82 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -365,9 +365,11 @@
 
             if (hasSystemUiObject("keyguard_status_view")) return "Phone is locked";
 
-            if (!mDevice.hasObject(By.textStartsWith(""))) return "Screen is empty";
+            if (!mDevice.wait(Until.hasObject(By.textStartsWith("")), WAIT_TIME_MS)) {
+                return "Screen is empty";
+            }
 
-            final String navigationModeError = getNavigationModeMismatchError();
+            final String navigationModeError = getNavigationModeMismatchError(true);
             if (navigationModeError != null) return navigationModeError;
         } catch (Throwable e) {
             Log.w(TAG, "getSystemAnomalyMessage failed", e);
@@ -535,17 +537,28 @@
         mExpectedRotation = expectedRotation;
     }
 
-    public String getNavigationModeMismatchError() {
+    public String getNavigationModeMismatchError(boolean waitForCorrectState) {
+        final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
         final NavigationModel navigationModel = getNavigationModel();
-        final boolean hasRecentsButton = hasSystemUiObject("recent_apps");
-        final boolean hasHomeButton = hasSystemUiObject("home");
-        if ((navigationModel == NavigationModel.THREE_BUTTON) != hasRecentsButton) {
-            return "Presence of recents button doesn't match the interaction mode, mode="
-                    + navigationModel.name() + ", hasRecents=" + hasRecentsButton;
+
+        if (navigationModel == NavigationModel.THREE_BUTTON) {
+            if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+                return "Recents button not present in 3-button mode";
+            }
+        } else {
+            if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+                return "Recents button is present in non-3-button mode";
+            }
         }
-        if ((navigationModel != NavigationModel.ZERO_BUTTON) != hasHomeButton) {
-            return "Presence of home button doesn't match the interaction mode, mode="
-                    + navigationModel.name() + ", hasHome=" + hasHomeButton;
+
+        if (navigationModel == NavigationModel.ZERO_BUTTON) {
+            if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+                return "Home button is present in gestural mode";
+            }
+        } else {
+            if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+                return "Home button not present in non-gestural mode";
+            }
         }
         return null;
     }
@@ -556,7 +569,7 @@
         assertEquals("Unexpected display rotation",
                 mExpectedRotation, mDevice.getDisplayRotation());
 
-        final String error = getNavigationModeMismatchError();
+        final String error = getNavigationModeMismatchError(true);
         assertTrue(error, error == null);
 
         log("verifyContainerType: " + containerType);
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index f0e686f..d43e235 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -110,7 +110,8 @@
                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
             LauncherInstrumentation.log(
-                    "switchToAllApps: swipeHeight = " + swipeHeight + ", slop = "
+                    "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
+                            + ", swipeHeight = " + swipeHeight + ", slop = "
                             + mLauncher.getTouchSlop());
 
             mLauncher.swipeToState(