Merge "Remove LiveTileOverlay" into sc-dev
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..544bc99 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;
@@ -166,6 +172,14 @@
             public View.OnLongClickListener getItemOnLongClickListener() {
                 return mDragController::startDragOnLongClick;
             }
+
+            @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 +221,8 @@
         mTaskbarVisibilityController.init();
         mHotseatController.init();
         mRecentsController.init();
+
+        SCALE_PROPERTY.set(mTaskbarView, mLauncher.hasBeenResumed() ? getTaskbarScaleOnHome() : 1f);
     }
 
     private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
@@ -290,11 +306,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 +421,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 +478,7 @@
     protected interface TaskbarViewCallbacks {
         View.OnClickListener getItemOnClickListener();
         View.OnLongClickListener getItemOnLongClickListener();
+        int getEmptyHotseatViewVisibility();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 7a13b89..ed27f2f 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,44 @@
                     && 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);
             }
+            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 +415,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/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/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/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/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 64d05e1..357aef1 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;
@@ -516,14 +516,26 @@
     }
 
     @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");
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 39751c0..17822e6 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -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;
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/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 584a284..9537247 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -16,7 +16,6 @@
 package com.android.quickstep.util;
 
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 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;
@@ -98,6 +97,7 @@
     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();
@@ -317,9 +317,10 @@
         mMatrix.postScale(scale, scale, mIsRecentsRtl ? 0 : taskWidth, 0);
         float taskWidthDiff = taskWidth * (1 - gridScale);
         float taskWidthOffset = mIsRecentsRtl ? taskWidthDiff : -taskWidthDiff;
-        float translationPrimary = Utilities.mapRange(interpolatedGridProgress, 0, taskWidthOffset);
         mOrientationState.getOrientationHandler().set(mMatrix, MATRIX_POST_TRANSLATE,
-                translationPrimary);
+                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);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 156ac47..f20ca82 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -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,6 +105,7 @@
 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;
@@ -822,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;
@@ -883,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));
@@ -899,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--;
@@ -925,6 +948,8 @@
             // 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.
+            mLiveTileTaskViewSimulator.taskPrimaryTranslation.value = 0;
+            mLiveTileTaskViewSimulator.taskSecondaryTranslation.value = 0;
             mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
             mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
         }
@@ -2711,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) {
@@ -2727,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/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index fdcb8dc..0cf7261 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -916,11 +916,19 @@
         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();
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/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/values/strings.xml b/res/values/strings.xml
index 73f9e53..44b5ee7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -52,11 +52,22 @@
     <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>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 7beea74..f2dd60e 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -641,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
@@ -651,9 +652,10 @@
                         ((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 {
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 4f4f2a7..ebaacb6 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -98,14 +98,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);
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 781f171..21c40ef 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -59,6 +59,7 @@
     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 =
@@ -188,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
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 981ead9..77fee08 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -446,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;
@@ -1877,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 {
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/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/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 72bbc43..914772d 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -107,4 +107,5 @@
     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/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
index a5ed20a..95fa05f 100644
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
+++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
@@ -16,10 +16,10 @@
 package com.android.launcher3.widget.picker;
 
 import android.view.View;
+import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.RelativeLayout;
 
 import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.widget.picker.WidgetsFullSheet.SearchAndRecommendationViewHolder;
@@ -33,20 +33,21 @@
         RecyclerViewFastScroller.OnFastScrollChangeListener {
     private final boolean mHasWorkProfile;
     private final SearchAndRecommendationViewHolder mViewHolder;
-    private final RecyclerView mPrimaryRecyclerView;
+    private final WidgetsRecyclerView mPrimaryRecyclerView;
 
     // The following are only non null if mHasWorkProfile is true.
-    @Nullable private final RecyclerView mWorkRecyclerView;
+    @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,
-            RecyclerView primaryRecyclerView,
-            @Nullable RecyclerView workRecyclerView,
+            WidgetsRecyclerView primaryRecyclerView,
+            @Nullable WidgetsRecyclerView workRecyclerView,
             @Nullable View personalWorkTabsView,
             @Nullable PersonalWorkPagedView primaryWorkViewPager) {
         mHasWorkProfile = hasWorkProfile;
@@ -55,6 +56,12 @@
         mWorkRecyclerView = workRecyclerView;
         mPrimaryWorkTabsView = personalWorkTabsView;
         mPrimaryWorkViewPager = primaryWorkViewPager;
+        mCurrentRecyclerView = mPrimaryRecyclerView;
+    }
+
+    /** Sets the current active {@link WidgetsRecyclerView}. */
+    public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
+        mCurrentRecyclerView = currentRecyclerView;
     }
 
     /**
@@ -64,10 +71,10 @@
         // 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()
-                + mViewHolder.mCollapseHandle.getMeasuredHeight()
-                + mViewHolder.mHeaderTitle.getMeasuredHeight();
+                + measureHeightWithVerticalMargins(mViewHolder.mCollapseHandle)
+                + measureHeightWithVerticalMargins(mViewHolder.mHeaderTitle);
 
-        int topContainerHeight = mViewHolder.mContainer.getMeasuredHeight();
+        int topContainerHeight = measureHeightWithVerticalMargins(mViewHolder.mContainer);
         if (mHasWorkProfile) {
             // In a work profile setup, the full widget sheet contains the following views:
             //           -------               -|
@@ -114,8 +121,8 @@
             //
             // When the views are first inflated, the sum of topOffsetAfterAllViewsCollapsed and
             // mMaxCollapsibleDistance should equal to the top container height.
-            int tabsViewActualHeight =
-                    mPrimaryWorkTabsView.getMeasuredHeight() - mPrimaryWorkTabsView.getPaddingTop();
+            int tabsViewActualHeight = measureHeightWithVerticalMargins(mPrimaryWorkTabsView)
+                    - mPrimaryWorkTabsView.getPaddingTop();
             int topOffsetAfterAllViewsCollapsed =
                     topContainerHeight + tabsViewActualHeight - mMaxCollapsibleHeight;
 
@@ -149,9 +156,12 @@
      * views (e.g. recycler views, tabs) upon scrolling.
      */
     @Override
-    public void onThumbOffsetYChanged(int y) {
+    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(-y, -mMaxCollapsibleHeight);
+            int yDisplacement = Math.max(-recyclerViewYOffset, -mMaxCollapsibleHeight);
             mViewHolder.mHeaderTitle.setTranslationY(yDisplacement);
             mViewHolder.mSearchBar.setTranslationY(yDisplacement);
             if (mHasWorkProfile) {
@@ -159,4 +169,20 @@
             }
         }
     }
+
+    /** 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 91b79f9..52a2fc5 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -149,7 +149,10 @@
 
     @Override
     public void onActivePageChanged(int currentActivePage) {
-        mAdapters.get(currentActivePage).mWidgetsRecyclerView.bindFastScrollbar();
+        WidgetsRecyclerView currentRecyclerView =
+                mAdapters.get(currentActivePage).mWidgetsRecyclerView;
+        currentRecyclerView.bindFastScrollbar();
+        mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView);
 
         reset();
     }
@@ -159,6 +162,7 @@
         if (mHasWorkProfile) {
             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
         }
+        mSearchAndRecommendationsScrollController.reset();
     }
 
     @VisibleForTesting
@@ -241,6 +245,12 @@
             mAdapters.get(AdapterHolder.WORK).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
                     maxSpansPerRow);
         }
+
+        if (mInitialTabsHeight == 0 && mTabsView != null) {
+            mInitialTabsHeight = measureHeightWithVerticalMargins(mTabsView);
+        }
+
+        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
     }
 
     @Override
@@ -255,12 +265,6 @@
                 contentLeft + contentWidth, height);
 
         setTranslationShift(mTranslationShift);
-
-        if (mInitialTabsHeight == 0 && mTabsView != null) {
-            mInitialTabsHeight = mTabsView.getMeasuredHeight();
-        }
-
-        mSearchAndRecommendationsScrollController.updateMarginAndPadding();
     }
 
     @Override
@@ -371,7 +375,14 @@
         // No need to check work profile here because mInitialTabHeight is always 0 if there is no
         // work profile.
         return mInitialTabsHeight
-                + mSearchAndRecommendationViewHolder.mContainer.getMeasuredHeight();
+                + 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. */
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/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/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index 5e41d43d..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 -> {
@@ -157,7 +175,6 @@
             if (((AllAppsPagedView) l.getAppsView().getContentView()).getCurrentPage()
                     != WORK_PAGE) {
                 Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Work page not highlighted");
-                return false;
             }
             return ((TextView) workEduView.findViewById(R.id.content_text)).getText().equals(
                     l.getResources().getString(R.string.work_profile_edu_work_apps));