Merge "Support fade-in divider bar with quick switch gesture" into tm-dev
diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
index 5c66944..9f3be69 100644
--- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java
+++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
@@ -62,6 +62,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import com.android.launcher3.logger.LauncherAtom.FolderContainer;
@@ -116,7 +117,8 @@
     @AnyThread
     private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId,
             int targetPredictor) {
-        if (target != null) {
+        // TODO: remove the running test check when b/231648228 is fixed.
+        if (target != null && !Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
                     .setLaunchLocation(getContainer(locationInfo))
                     .build();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 138fb99..2be1179 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -372,6 +372,7 @@
 
         // If we're already animating to the value, just leave it be instead of restarting it.
         if (!mIconAlignmentForLauncherState.isAnimatingToValue(toAlignment)) {
+            mIconAlignmentForLauncherState.finishAnimation();
             animatorSet.play(mIconAlignmentForLauncherState.animateToValue(toAlignment)
                     .setDuration(duration));
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 2d7ce32..c4837a0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -62,7 +62,7 @@
                     PropertyValuesHolder.ofFloat(TRANSLATION_SHIFT, TRANSLATION_SHIFT_OPENED));
             mOpenCloseAnimator.setInterpolator(AGGRESSIVE_EASE);
             mOpenCloseAnimator.setDuration(
-                    ALL_APPS.getTransitionDuration(mContext, true /* isToState */)).start();
+                    ALL_APPS.getTransitionDuration(mActivityContext, true /* isToState */)).start();
         } else {
             mTranslationShift = TRANSLATION_SHIFT_OPENED;
         }
@@ -81,7 +81,8 @@
     @Override
     protected void handleClose(boolean animate) {
         Optional.ofNullable(mOnCloseBeginListener).ifPresent(OnCloseListener::onSlideInViewClosed);
-        handleClose(animate, ALL_APPS.getTransitionDuration(mContext, false /* isToState */));
+        handleClose(animate,
+                ALL_APPS.getTransitionDuration(mActivityContext, false /* isToState */));
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index c25a815..7b01670 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 
+import com.android.launcher3.DeviceProfile.DeviceProfileListenable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -38,8 +39,11 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
-        return isToState ? 500 : 300;
+    public <DEVICE_PROFILE_CONTEXT extends Context & DeviceProfileListenable>
+    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState) {
+        return !context.getDeviceProfile().isTablet && isToState
+                ? 600
+                : isToState ? 500 : 300;
     }
 
     @Override
@@ -89,9 +93,9 @@
 
     @Override
     public int getVisibleElements(Launcher launcher) {
-        // Don't add HOTSEAT_ICONS for phones in ALL_APPS state.
-        return launcher.getDeviceProfile().isPhone ? ALL_APPS_CONTENT
-                : ALL_APPS_CONTENT | HOTSEAT_ICONS;
+        // Don't add HOTSEAT_ICONS for non-tablets in ALL_APPS state.
+        return launcher.getDeviceProfile().isTablet ? ALL_APPS_CONTENT | HOTSEAT_ICONS
+                : ALL_APPS_CONTENT;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 699ce97..5f62749 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -66,9 +66,9 @@
     @Override
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
         RecentsView recentsView = launcher.getOverviewPanel();
-        float workspacePageWidth = launcher.getDeviceProfile().getWorkspaceWidth();
+        float workspacePageHeight = launcher.getDeviceProfile().getCellLayoutHeight();
         recentsView.getTaskSize(sTempRect);
-        float scale = (float) sTempRect.width() / workspacePageWidth;
+        float scale = (float) sTempRect.height() / workspacePageHeight;
         float parallaxFactor = 0.5f;
         return new ScaleAndTranslation(scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor);
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 2ca59eb..53dc9dd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
 import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
@@ -451,9 +452,11 @@
                 .withSrcState(LAUNCHER_STATE_HOME)
                 .withDstState(targetState.statsLogOrdinal)
                 .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
-                        targetState.ordinal > mStartState.ordinal
-                                ? LAUNCHER_UNKNOWN_SWIPEUP
-                                : LAUNCHER_UNKNOWN_SWIPEDOWN));
+                        targetState == QUICK_SWITCH
+                                ? LAUNCHER_QUICKSWITCH_RIGHT
+                                : targetState.ordinal > mStartState.ordinal
+                                        ? LAUNCHER_UNKNOWN_SWIPEUP
+                                        : LAUNCHER_UNKNOWN_SWIPEDOWN));
         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 1504c12..dbee9c1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -261,7 +261,7 @@
 
     @Override
     protected void onReachedFinalState(LauncherState toState) {
-        super.onReinitToState(toState);
+        super.onReachedFinalState(toState);
         if (toState == ALL_APPS) {
             InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
         }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 5acce89..13389c0 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,6 +22,7 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.PagedView.INVALID_PAGE;
 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;
@@ -66,6 +67,7 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
@@ -926,7 +928,13 @@
             mLogDirectionUpOrLeft = velocity.x < 0;
         }
         mDownPos = downPos;
-        handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
+        Runnable handleNormalGestureEndCallback = () ->
+                handleNormalGestureEnd(endVelocity, isFling, velocity, /* isCancel= */ false);
+        if (mRecentsView != null) {
+            mRecentsView.runOnPageScrollsInitialized(handleNormalGestureEndCallback);
+        } else {
+            handleNormalGestureEndCallback.run();
+        }
     }
 
     private void endRunningWindowAnim(boolean cancel) {
@@ -1130,6 +1138,13 @@
         } else if (endTarget == RECENTS) {
             if (mRecentsView != null) {
                 int nearestPage = mRecentsView.getDestinationPage();
+                if (nearestPage == INVALID_PAGE) {
+                    // Allow the snap to invalid page to catch future error cases.
+                    Log.e(TAG,
+                            "RecentsView destination page is invalid",
+                            new IllegalStateException());
+                }
+
                 boolean isScrolling = false;
                 if (mRecentsView.getNextPage() != nearestPage) {
                     // We shouldn't really scroll to the next page when swiping up to recents.
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 48127c0..6745246 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -84,6 +84,8 @@
 
     private STATE_TYPE mTargetState;
 
+    @Nullable private Runnable mOnInitBackgroundStateUICallback = null;
+
     protected BaseActivityInterface(boolean rotationSupportedByActivity,
             STATE_TYPE overviewState, STATE_TYPE backgroundState) {
         this.rotationSupportedByActivity = rotationSupportedByActivity;
@@ -408,6 +410,21 @@
         return null;
     }
 
+    protected void runOnInitBackgroundStateUI(Runnable callback) {
+        mOnInitBackgroundStateUICallback = callback;
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity != null && activity.getStateManager().getState() == mBackgroundState) {
+            onInitBackgroundStateUI();
+        }
+    }
+
+    private void onInitBackgroundStateUI() {
+        if (mOnInitBackgroundStateUICallback != null) {
+            mOnInitBackgroundStateUICallback.run();
+            mOnInitBackgroundStateUICallback = null;
+        }
+    }
+
     public interface AnimationFactory {
 
         void createActivityInterface(long transitionLength);
@@ -447,13 +464,14 @@
             mStartState = mActivity.getStateManager().getState();
         }
 
-        protected ACTIVITY_TYPE initUI() {
+        protected ACTIVITY_TYPE initBackgroundStateUI() {
             STATE_TYPE resetState = mStartState;
             if (mStartState.shouldDisableRestore()) {
                 resetState = mActivity.getStateManager().getRestState();
             }
             mActivity.getStateManager().setRestState(resetState);
             mActivity.getStateManager().goToState(mBackgroundState, false);
+            onInitBackgroundStateUI();
             return mActivity;
         }
 
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 7feec2c..ba61574 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -89,7 +89,7 @@
             boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
-        factory.initUI();
+        factory.initBackgroundStateUI();
         return factory;
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 10a3a2e..c13b95f 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -135,7 +135,7 @@
             }
         };
 
-        BaseQuickstepLauncher launcher = factory.initUI();
+        BaseQuickstepLauncher launcher = factory.initBackgroundStateUI();
         // Since all apps is not visible, we can safely reset the scroll position.
         // This ensures then the next swipe up to all-apps starts from scroll 0.
         launcher.getAppsView().reset(false /* animate */);
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index dc65b50..fd9f922 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep;
 
+import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
 import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
@@ -37,6 +38,7 @@
 import android.window.BackEvent;
 import android.window.IOnBackInvokedCallback;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.QuickstepTransitionManager;
 import com.android.launcher3.R;
@@ -274,6 +276,10 @@
             mLauncher.getStateManager().moveToRestState();
         }
 
+        // Explicitly close opened floating views (which is typically called from
+        // Launcher#onResumed, but in the predictive back flow launcher is not resumed until
+        // the transition is fully finished.)
+        AbstractFloatingView.closeAllOpenViewsExcept(mLauncher, false, TYPE_REBIND_SAFE);
         float cornerRadius = Utilities.mapRange(
                 mBackProgress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
         Pair<RectFSpringAnim, AnimatorSet> pair =
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 42f9eb6..dffdc5a 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -217,7 +217,8 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
-                interactionHandler.onGestureEnded(0, new PointF(), new PointF());
+                activityInterface.runOnInitBackgroundStateUI(() ->
+                        interactionHandler.onGestureEnded(0, new PointF(), new PointF()));
                 cmd.removeListener(this);
             }
 
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ef81449..528fb97 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -66,6 +66,12 @@
                         mDeviceProfile.overviewPageSpacing);
                 return response;
             }
+
+            case TestProtocol.REQUEST_HAS_TIS: {
+                response.putBoolean(
+                        TestProtocol.REQUEST_HAS_TIS, true);
+                return response;
+            }
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 10c56c9..85ef6cb 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -130,7 +130,7 @@
         if (IS_VERBOSE) {
             Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
         }
-        if (!Utilities.ATLEAST_R) {
+        if (!Utilities.ATLEAST_R || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return;
         }
         SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT,
@@ -415,6 +415,10 @@
                 consumer.consume(event, atomInfo);
             }
 
+            // TODO: remove this when b/231648228 is fixed.
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                return;
+            }
             SysUiStatsLog.write(
                     SysUiStatsLog.LAUNCHER_EVENT,
                     SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
@@ -510,6 +514,9 @@
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         switch (info.getContainerInfo().getContainerCase()) {
             case PREDICTED_HOTSEAT_CONTAINER:
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
@@ -625,6 +632,9 @@
     }
 
     private static int getHierarchy(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         if (info.getContainerInfo().getContainerCase() == FOLDER) {
             return info.getContainerInfo().getFolder().getParentContainerCase().getNumber()
                     + FOLDER_HIERARCHY_OFFSET;
@@ -665,6 +675,9 @@
     }
 
     private static int getSearchAttributes(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         ContainerInfo containerInfo = info.getContainerInfo();
         if (containerInfo.getContainerCase() == EXTENDED_CONTAINERS
                 && containerInfo.getExtendedContainers().getContainerCase()
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 72f4221..b634518 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -946,7 +946,7 @@
                 mHasVisibleTaskData.delete(i);
             }
             if (child instanceof GroupedTaskView) {
-                mGroupedTaskViewPool.recycle((GroupedTaskView)taskView);
+                mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
             } else {
                 mTaskViewPool.recycle(taskView);
             }
@@ -1855,17 +1855,18 @@
         if (!mActivity.getDeviceProfile().isTablet) {
             return super.getDestinationPage(scaledScroll);
         }
-
-        final int childCount = getChildCount();
-        if (mPageScrolls == null || childCount != mPageScrolls.length) {
-            return -1;
+        if (!pageScrollsInitialized()) {
+            Log.e(TAG,
+                    "Cannot get destination page: RecentsView not properly initialized",
+                    new IllegalStateException());
+            return INVALID_PAGE;
         }
 
         // When in tablet with variable task width, 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 minDistanceFromScreenStartIndex = INVALID_PAGE;
+        for (int i = 0; i < getChildCount(); ++i) {
             int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
             if (distanceFromScreenStart < minDistanceFromScreenStart) {
                 minDistanceFromScreenStart = distanceFromScreenStart;
@@ -4051,7 +4052,7 @@
         // TODO(194414938) starting bounds seem slightly off, investigate
         Rect firstTaskStartingBounds = new Rect();
         Rect firstTaskEndingBounds = mTempRect;
-        int duration = mActivity.getStateManager().getState().getTransitionDuration(getContext(),
+        int duration = mActivity.getStateManager().getState().getTransitionDuration(mActivity,
                 false /* isToState */);
         PendingAnimation pendingAnimation = new PendingAnimation(duration);
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 4d38822..4bf247c 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -52,6 +52,8 @@
 public class TaplTestsQuickstep extends AbstractQuickStepTest {
 
     private static final String APP_NAME = "LauncherTestApp";
+    private static final String CALCULATOR_APP_PACKAGE =
+            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
 
     @Before
     public void setUp() throws Exception {
@@ -73,7 +75,7 @@
 
     public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         startTestActivity(2);
     }
 
@@ -207,7 +209,7 @@
     @NavigationModeSwitch
     @PortraitLandscape
     public void testBackground() throws Exception {
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
 
         assertNotNull("Background.switchToOverview() returned null",
@@ -302,7 +304,7 @@
         mLauncher.getWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
 
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         mLauncher.pressBack();
         mLauncher.getWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index b9942c0..81e94f7 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -18,10 +18,11 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
+    <solid android:color="?androidprv:attr/colorSurfaceVariant" />
     <corners android:radius="@dimen/rounded_button_radius" />
     <stroke
         android:width="1dp"
-        android:color="?androidprv:attr/colorAccentPrimaryVariant" />
+        android:color="?androidprv:attr/colorSurfaceVariant" />
     <padding
         android:left="@dimen/rounded_button_padding"
         android:right="@dimen/rounded_button_padding" />
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 0f6fc6c..feebfe1 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -22,7 +22,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_weight="1"
-        android:importantForAccessibility="no"
+        android:importantForAccessibility="noHideDescendants"
         android:layout_marginVertical="8dp">
         <!-- The image of the widget. This view does not support padding. Any placement adjustment
              should be done using margins. Width & height are set at runtime after scaling the
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index 8f0eae7..3cdc2e8 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -21,7 +21,10 @@
     android:layout_height="wrap_content"
     android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
     android:orientation="horizontal"
-    launcher:appIconSize="48dp">
+    android:importantForAccessibility="yes"
+    android:focusable="true"
+    launcher:appIconSize="48dp"
+    android:descendantFocusability="afterDescendants">
 
     <ImageView
         android:id="@+id/app_icon"
@@ -32,14 +35,11 @@
         tools:src="@drawable/ic_corp"/>
 
     <LinearLayout
-        android:id="@+id/app_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:layout_weight="1"
-        android:orientation="vertical"
-        android:focusable="true"
-        android:descendantFocusability="afterDescendants">
+        android:orientation="vertical">
 
         <TextView
             android:id="@+id/app_title"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index 73200de..f3b3053 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -16,9 +16,9 @@
 <com.android.launcher3.allapps.WorkEduCard xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginTop="8dp"
+    android:layout_marginTop="@dimen/work_edu_card_margin"
+    android:layout_marginBottom="@dimen/work_edu_card_bottom_margin"
     android:gravity="center">
-
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -36,30 +36,29 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginBottom="@dimen/work_card_margin"
-            android:layout_marginRight="@dimen/work_card_margin"
+            android:layout_marginEnd="@dimen/work_card_margin"
             android:text="@string/work_profile_edu_work_apps"
+            android:textDirection="locale"
             android:textSize="18sp" />
         <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="@dimen/padded_rounded_button_height"
-            android:orientation="horizontal"
-            >
+            android:orientation="horizontal">
             <FrameLayout
                 android:layout_width="@dimen/rounded_button_width"
                 android:layout_height="@dimen/rounded_button_width"
+                android:layout_alignParentEnd="true"
                 android:background="@drawable/rounded_action_button"
-                android:padding="@dimen/rounded_button_padding"
-                android:layout_alignParentRight="true">
+                android:padding="@dimen/rounded_button_padding">
                 <ImageButton
+                    android:id="@+id/action_btn"
                     android:layout_width="@dimen/x_icon_size"
                     android:layout_height="@dimen/x_icon_size"
-                    android:id="@+id/action_btn"
-                    android:src="@drawable/ic_remove_no_shadow"
                     android:layout_gravity="center"
-                    android:padding="@dimen/x_icon_padding" />
+                    android:padding="@dimen/x_icon_padding"
+                    android:src="@drawable/ic_remove_no_shadow" />
             </FrameLayout>
         </RelativeLayout>
-
     </RelativeLayout>
 
 
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index c536d77..d2fa5fa 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -21,14 +21,14 @@
     android:layout_width="wrap_content"
     android:gravity="center"
     android:includeFontPadding="false"
+    android:textDirection="locale"
     android:drawableTint="@color/all_apps_tab_text"
     android:textColor="@color/all_apps_tab_text"
     android:textSize="14sp"
     android:background="@drawable/work_apps_toggle_background"
     android:drawablePadding="8dp"
     android:drawableStart="@drawable/ic_corp_off"
-    android:layout_marginBottom="@dimen/work_fab_margin"
-    android:layout_marginEnd="@dimen/work_fab_margin"
+    android:layout_marginBottom="@dimen/work_fab_margin_bottom"
     android:paddingLeft="@dimen/work_mode_fab_padding"
     android:paddingRight="@dimen/work_mode_fab_padding"
     android:text="@string/work_apps_pause_btn_text" />
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2c3f5ed..a8ee721 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -129,6 +129,7 @@
     <dimen name="all_apps_height_extra">6dp</dimen>
     <dimen name="all_apps_bottom_sheet_horizontal_padding">0dp</dimen>
     <dimen name="all_apps_paged_view_top_padding">40dp</dimen>
+    <dimen name="all_apps_personal_work_tabs_vertical_margin">16dp</dimen>
 
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
@@ -148,10 +149,12 @@
     <dimen name="work_card_padding_horizontal">10dp</dimen>
     <dimen name="work_card_button_height">52dp</dimen>
     <dimen name="work_fab_margin">16dp</dimen>
+    <dimen name="work_fab_margin_bottom">20dp</dimen>
     <dimen name="work_mode_fab_padding">16dp</dimen>
     <dimen name="work_profile_footer_padding">20dp</dimen>
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
+    <dimen name="work_edu_card_bottom_margin">26dp</dimen>
 
     <dimen name="work_card_margin">24dp</dimen>
     <!-- (x) icon button inside work edu card -->
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ffa1e3f..ee5e024 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -428,7 +428,7 @@
     <string name="work_apps_paused_edu_accept">Got it</string>
 
     <!-- button string shown pause work profile -->
-    <string name="work_apps_pause_btn_text">Turn off work apps</string>
+    <string name="work_apps_pause_btn_text">Pause work apps</string>
     <!-- button string shown enable work profile -->
     <string name="work_apps_enable_btn_text">Turn on work apps</string>
 
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 9369bdc..b6d3fc5 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -23,7 +23,6 @@
 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;
@@ -197,13 +196,6 @@
         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/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 88030ae..561f9b9 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -501,7 +501,7 @@
      */
     private int calculateQsbWidth() {
         if (isQsbInline) {
-            int columns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns;
+            int columns = getPanelCount() * inv.numColumns;
             return getIconToIconWidthForColumns(columns)
                     - iconSizePx * numShownHotseatIcons
                     - hotseatBorderSpace * numShownHotseatIcons;
@@ -665,11 +665,10 @@
         updateIconSize(1f, res);
 
         updateWorkspacePadding();
-        Point workspacePadding = getTotalWorkspacePadding();
 
         // Check to see if the icons fit within the available height.
         float usedHeight = getCellLayoutHeightSpecification();
-        final int maxHeight = getWorkspaceHeight(workspacePadding);
+        final int maxHeight = getCellLayoutHeight();
         float extraHeight = Math.max(0, maxHeight - usedHeight);
         float scaleY = maxHeight / usedHeight;
         boolean shouldScale = scaleY < 1f;
@@ -702,7 +701,7 @@
     }
 
     private int getCellLayoutWidthSpecification() {
-        int numColumns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns;
+        int numColumns = getPanelCount() * inv.numColumns;
         return (cellWidthPx * numColumns) + (cellLayoutBorderSpacePx.x * (numColumns - 1))
                 + cellLayoutPaddingPx.left + cellLayoutPaddingPx.right;
     }
@@ -902,19 +901,21 @@
             result = new Point();
         }
 
-        // Since we are only concerned with the overall padding, layout direction does
-        // not matter.
-        Point padding = getTotalWorkspacePadding();
-
-        int numColumns = isTwoPanels ? inv.numColumns * 2 : inv.numColumns;
-        int screenWidthPx = getWorkspaceWidth(padding);
-        result.x = calculateCellWidth(screenWidthPx, cellLayoutBorderSpacePx.x, numColumns);
-        int screenHeightPx = getWorkspaceHeight(padding);
-        result.y = calculateCellHeight(screenHeightPx, cellLayoutBorderSpacePx.y, inv.numRows);
+        result.x = calculateCellWidth(getShortcutAndWidgetContainerWidth(),
+                cellLayoutBorderSpacePx.x, inv.numColumns);
+        result.y = calculateCellHeight(getShortcutAndWidgetContainerHeight(),
+                cellLayoutBorderSpacePx.y, inv.numRows);
         return result;
     }
 
     /**
+     * Gets the number of panels within the workspace.
+     */
+    public int getPanelCount() {
+        return isTwoPanels ? 2 : 1;
+    }
+
+    /**
      * Gets the space in px from the bottom of last item in the vertical-bar hotseat to the
      * bottom of the screen.
      */
@@ -932,7 +933,7 @@
     /**
      * Gets the scaled top of the workspace in px for the spring-loaded edit state.
      */
-    public float getWorkspaceSpringLoadShrunkTop() {
+    public float getCellLayoutSpringLoadShrunkTop() {
         workspaceSpringLoadShrunkTop = mInsets.top + dropTargetBarTopMarginPx + dropTargetBarSizePx
                 + dropTargetBarBottomMarginPx;
         return workspaceSpringLoadShrunkTop;
@@ -941,7 +942,7 @@
     /**
      * Gets the scaled bottom of the workspace in px for the spring-loaded edit state.
      */
-    private float getWorkspaceSpringLoadShrunkBottom() {
+    private float getCellLayoutSpringLoadShrunkBottom() {
         int topOfHotseat = hotseatBarSizePx + springLoadedHotseatBarTopMarginPx;
         workspaceSpringLoadShrunkBottom =
                 heightPx - (isVerticalBarLayout() ? getVerticalHotseatLastItemBottomOffset()
@@ -960,9 +961,8 @@
      * Gets the scale of the workspace for the spring-loaded edit state.
      */
     public float getWorkspaceSpringLoadScale() {
-        float cellLayoutHeight = availableHeightPx - workspacePadding.top - workspacePadding.bottom;
-        float scale = (getWorkspaceSpringLoadShrunkBottom() - getWorkspaceSpringLoadShrunkTop())
-                / cellLayoutHeight;
+        float scale = (getCellLayoutSpringLoadShrunkBottom() - getCellLayoutSpringLoadShrunkTop())
+                / getCellLayoutHeight();
         scale = Math.min(scale, 1f);
 
         // Reduce scale if next pages would not be visible after scaling the workspace
@@ -976,19 +976,55 @@
         return scale;
     }
 
+    /**
+     * Gets the width of the Workspace, aka a scrollable page of the homescreen.
+     */
     public int getWorkspaceWidth() {
-        return getWorkspaceWidth(getTotalWorkspacePadding());
+        return availableWidthPx;
     }
 
-    public int getWorkspaceWidth(Point workspacePadding) {
-        int cellLayoutTotalPadding =
-                (isTwoPanels ? 2 : 1) * (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right);
-        return availableWidthPx - workspacePadding.x - cellLayoutTotalPadding;
+    /**
+     * Gets the height of the Workspace, aka a scrollable page of the homescreen.
+     */
+    public int getWorkspaceHeight() {
+        return availableHeightPx;
     }
 
-    private int getWorkspaceHeight(Point workspacePadding) {
-        return availableHeightPx - workspacePadding.y - (cellLayoutPaddingPx.top
-                + cellLayoutPaddingPx.bottom);
+    /**
+     * Gets the width of a single Cell Layout, aka a single panel within a Workspace.
+     *
+     * <p>This is the width of a Workspace, less its horizontal padding. Note that two-panel
+     * layouts have two Cell Layouts per workspace.
+     */
+    public int getCellLayoutWidth() {
+        return (getWorkspaceWidth() - getTotalWorkspacePadding().x) / getPanelCount();
+    }
+
+    /**
+     * Gets the height of a single Cell Layout, aka a single panel within a Workspace.
+     *
+     * <p>This is the height of a Workspace, less its vertical padding.
+     */
+    public int getCellLayoutHeight() {
+        return getWorkspaceHeight() - getTotalWorkspacePadding().y;
+    }
+
+    /**
+     * Gets the width of the container holding the shortcuts and widgets.
+     *
+     * <p>This is the width of one Cell Layout less its horizontal padding.
+     */
+    public int getShortcutAndWidgetContainerWidth() {
+        return getCellLayoutWidth() - (cellLayoutPaddingPx.left + cellLayoutPaddingPx.right);
+    }
+
+    /**
+     * Gets the height of the container holding the shortcuts and widgets.
+     *
+     * <p>This is the height of one Cell Layout less its vertical padding.
+     */
+    public int getShortcutAndWidgetContainerHeight() {
+        return getCellLayoutHeight() - (cellLayoutPaddingPx.top + cellLayoutPaddingPx.bottom);
     }
 
     public Point getTotalWorkspacePadding() {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 89b1771..0373eef 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -61,6 +61,8 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -628,8 +630,21 @@
 
         float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density;
         float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;
-        return getBestMatch(screenWidth, screenHeight,
-                WindowManagerProxy.INSTANCE.get(context).getRotation(context));
+        int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context);
+
+        if (Utilities.IS_DEBUG_DEVICE) {
+            StringWriter stringWriter = new StringWriter();
+            PrintWriter printWriter = new PrintWriter(stringWriter);
+            DisplayController.INSTANCE.get(context).dump(printWriter);
+            printWriter.flush();
+            Log.d("b/231312158", "getDeviceProfile -"
+                            + "\nconfig: " + config
+                            + "\ndisplayMetrics: " + res.getDisplayMetrics()
+                            + "\nrotation: " + rotation
+                            + "\n" + stringWriter.toString(),
+                    new Exception());
+        }
+        return getBestMatch(screenWidth, screenHeight, rotation);
     }
 
     /**
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2ad1d47..ad87451 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1279,13 +1279,16 @@
      * @param info The data structure describing the shortcut.
      */
     View createShortcut(WorkspaceItemInfo info) {
-        return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info);
+        // This can be called before PagedView#pageScrollsInitialized returns true, so use the
+        // first page, which we always assume to be present.
+        return createShortcut((ViewGroup) mWorkspace.getChildAt(0), info);
     }
 
     /**
      * Creates a view representing a shortcut inflated from the specified resource.
      *
-     * @param parent The group the shortcut belongs to.
+     * @param parent The group the shortcut belongs to. This is not necessarily the group where
+     *               the shortcut should be added.
      * @param info   The data structure describing the shortcut.
      * @return A View inflated from layoutResId.
      */
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 0a1d25c..95a8a2a 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -129,7 +129,10 @@
     private boolean mAllowEasyFling;
     protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
 
-    protected int[] mPageScrolls;
+    private final ArrayList<Runnable> mOnPageScrollsInitializedCallbacks = new ArrayList<>();
+
+    // We should always check pageScrollsInitialized() is true when using mPageScrolls.
+    @Nullable protected int[] mPageScrolls = null;
     private boolean mIsBeingDragged;
 
     // The amount of movement to begin scrolling
@@ -684,14 +687,37 @@
         setMeasuredDimension(widthSize, heightSize);
     }
 
+    /** Returns true iff this PagedView's scroll amounts are initialized to each page index. */
+    protected boolean pageScrollsInitialized() {
+        return mPageScrolls != null && mPageScrolls.length == getChildCount();
+    }
+
+    /**
+     * Queues the given callback to be run once {@code mPageScrolls} has been initialized.
+     */
+    public void runOnPageScrollsInitialized(Runnable callback) {
+        mOnPageScrollsInitializedCallbacks.add(callback);
+        if (pageScrollsInitialized()) {
+            onPageScrollsInitialized();
+        }
+    }
+
+    private void onPageScrollsInitialized() {
+        for (Runnable callback : mOnPageScrollsInitializedCallbacks) {
+            callback.run();
+        }
+        mOnPageScrollsInitializedCallbacks.clear();
+    }
+
     @SuppressLint("DrawAllocation")
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         mIsLayoutValid = true;
         final int childCount = getChildCount();
+        int[] pageScrolls = mPageScrolls;
         boolean pageScrollChanged = false;
-        if (mPageScrolls == null || childCount != mPageScrolls.length) {
-            mPageScrolls = new int[childCount];
+        if (!pageScrollsInitialized()) {
+            pageScrolls = new int[childCount];
             pageScrollChanged = true;
         }
 
@@ -701,10 +727,8 @@
 
         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
 
-        boolean isScrollChanged = getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC);
-        if (isScrollChanged) {
-            pageScrollChanged = true;
-        }
+        pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC);
+        mPageScrolls = pageScrolls;
 
         final LayoutTransition transition = getLayoutTransition();
         // If the transition is running defer updating max scroll, as some empty pages could
@@ -738,6 +762,7 @@
         if (mScroller.isFinished() && pageScrollChanged) {
             setCurrentPage(getNextPage());
         }
+        onPageScrollsInitialized();
     }
 
     /**
@@ -849,8 +874,10 @@
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
-        mCurrentPage = validateNewPage(mCurrentPage);
-        mCurrentScrollOverPage = mCurrentPage;
+        runOnPageScrollsInitialized(() -> {
+            mCurrentPage = validateNewPage(mCurrentPage);
+            mCurrentScrollOverPage = mCurrentPage;
+        });
         dispatchPageCountChanged();
     }
 
@@ -1153,7 +1180,7 @@
     }
 
     public int getScrollForPage(int index) {
-        if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) {
+        if (!pageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
             return 0;
         } else {
             return mPageScrolls[index];
@@ -1163,7 +1190,7 @@
     // While layout transitions are occurring, a child's position may stray from its baseline
     // position. This method returns the magnitude of this stray at any given time.
     public int getLayoutTransitionOffsetForPage(int index) {
-        if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) {
+        if (!pageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
             return 0;
         } else {
             View child = getChildAt(index);
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e279f59..47f2dd5 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -29,11 +29,13 @@
 import com.android.launcher3.DeviceProfile.DeviceProfileListenable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.views.AppLauncher;
 
+import java.util.ArrayList;
 import java.util.Objects;
 
 /**
@@ -95,11 +97,15 @@
         mHeader.reset(false);
     }
 
-    /** Invoke when the search results change. */
-    public void onSearchResultsChanged() {
-        for (int i = 0; i < mAH.size(); i++) {
-            if (mAH.get(i).mRecyclerView != null) {
-                mAH.get(i).mRecyclerView.onSearchResultsChanged();
+    /**
+     * Sets results list for search
+     */
+    public void setSearchResults(ArrayList<AdapterItem> results) {
+        if (getApps().setSearchResults(results)) {
+            for (int i = 0; i < mAH.size(); i++) {
+                if (mAH.get(i).mRecyclerView != null) {
+                    mAH.get(i).mRecyclerView.onSearchResultsChanged();
+                }
             }
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
index f97eb28..7067fa2 100644
--- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -37,10 +37,10 @@
      * Smooth scrolls the recycler view to the given section.
      */
     public void smoothScrollToSection(FastScrollSectionInfo info) {
-        if (mTargetFastScrollPosition == info.fastScrollToItem.position) {
+        if (mTargetFastScrollPosition == info.position) {
             return;
         }
-        mTargetFastScrollPosition = info.fastScrollToItem.position;
+        mTargetFastScrollPosition = info.position;
         mRv.getLayoutManager().startSmoothScroll(new MyScroller(mTargetFastScrollPosition));
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 7dbe711..bdfeada 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -71,6 +71,26 @@
         public void onChanged() {
             mCachedScrollPositions.clear();
         }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            onChanged();
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            onChanged();
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            onChanged();
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            onChanged();
+        }
     };
 
     // The empty-search result background
@@ -241,17 +261,14 @@
         // Find the fastscroll section that maps to this touch fraction
         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                 mApps.getFastScrollerSections();
-        AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
-        for (int i = 1; i < fastScrollSections.size(); i++) {
-            AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
-            if (info.touchFraction > touchFraction) {
-                break;
-            }
-            lastInfo = info;
+        int count = fastScrollSections.size();
+        if (count == 0) {
+            return "";
         }
-
-        mFastScrollHelper.smoothScrollToSection(lastInfo);
-        return lastInfo.sectionName;
+        int index = Utilities.boundToRange((int) (touchFraction * count), 0, count - 1);
+        AlphabeticalAppsList.FastScrollSectionInfo section = fastScrollSections.get(index);
+        mFastScrollHelper.smoothScrollToSection(section);
+        return section.sectionName;
     }
 
     @Override
@@ -272,12 +289,6 @@
     }
 
     @Override
-    protected float getBottomFadingEdgeStrength() {
-        // No bottom fading edge.
-        return 0;
-    }
-
-    @Override
     protected boolean isPaddingOffsetRequired() {
         return true;
     }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index cdaf80a..4ccfd39 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,9 +15,14 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ALL_APPS_DIVIDER;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_EMPTY_SEARCH;
+import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_SEARCH_MARKET;
 
 import android.content.Context;
 
+import androidx.recyclerview.widget.DiffUtil;
+
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.AppInfo;
@@ -52,14 +57,13 @@
      */
     public static class FastScrollSectionInfo {
         // The section name
-        public String sectionName;
-        // The AdapterItem to scroll to for this section
-        public AdapterItem fastScrollToItem;
-        // The touch fraction that should map to this fast scroll section info
-        public float touchFraction;
+        public final String sectionName;
+        // The item position
+        public final int position;
 
-        public FastScrollSectionInfo(String sectionName) {
+        public FastScrollSectionInfo(String sectionName, int position) {
             this.sectionName = sectionName;
+            this.position = position;
         }
     }
 
@@ -108,13 +112,6 @@
     }
 
     /**
-     * Returns all the apps.
-     */
-    public List<AppInfo> getApps() {
-        return mApps;
-    }
-
-    /**
      * Returns fast scroller sections of all the current filtered applications.
      */
     public List<FastScrollSectionInfo> getFastScrollerSections() {
@@ -235,77 +232,49 @@
      * mCachedSectionNames to have been calculated for the set of all apps in mApps.
      */
     public void updateAdapterItems() {
-        refillAdapterItems();
-        refreshRecyclerView();
-    }
-
-    private void refreshRecyclerView() {
-        if (mAdapter != null) {
-            mAdapter.notifyDataSetChanged();
-        }
-    }
-
-    private void refillAdapterItems() {
-        String lastSectionName = null;
-        FastScrollSectionInfo lastFastScrollerSectionInfo = null;
-        int position = 0;
-
+        List<AdapterItem> oldItems = new ArrayList<>(mAdapterItems);
         // Prepare to update the list of sections, filtered apps, etc.
-        mAccessibilityResultsCount = 0;
         mFastScrollerSections.clear();
         mAdapterItems.clear();
+        mAccessibilityResultsCount = 0;
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-
         if (!hasFilter()) {
-            mAccessibilityResultsCount = mApps.size();
+            int position = 0;
             if (mWorkAdapterProvider != null) {
                 position += mWorkAdapterProvider.addWorkItems(mAdapterItems);
                 if (!mWorkAdapterProvider.shouldShowWorkApps()) {
                     return;
                 }
             }
+            String lastSectionName = null;
             for (AppInfo info : mApps) {
-                String sectionName = info.sectionName;
+                mAdapterItems.add(AdapterItem.asApp(info));
 
+                String sectionName = info.sectionName;
                 // Create a new section if the section names do not match
                 if (!sectionName.equals(lastSectionName)) {
                     lastSectionName = sectionName;
-                    lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName);
-                    mFastScrollerSections.add(lastFastScrollerSectionInfo);
+                    mFastScrollerSections.add(new FastScrollSectionInfo(sectionName, position));
                 }
-
-                // Create an app item
-                AdapterItem appItem = AdapterItem.asApp(position++, info);
-                if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
-                    lastFastScrollerSectionInfo.fastScrollToItem = appItem;
-                }
-
-                mAdapterItems.add(appItem);
+                position++;
             }
         } else {
-            int count = mSearchResults.size();
-            for (int i = 0; i < count; i++) {
-                AdapterItem adapterItem = mSearchResults.get(i);
-                adapterItem.position = i;
-                mAdapterItems.add(adapterItem);
-
-                if (adapterItem.isCountedForAccessibility()) {
-                    mAccessibilityResultsCount++;
-                }
-            }
+            mAdapterItems.addAll(mSearchResults);
             if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()) {
                 // Append the search market item
                 if (hasNoFilteredResults()) {
-                    mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+                    mAdapterItems.add(new AdapterItem(VIEW_TYPE_EMPTY_SEARCH));
                 } else {
-                    mAdapterItems.add(AdapterItem.asAllAppsDivider(position++));
+                    mAdapterItems.add(new AdapterItem(VIEW_TYPE_ALL_APPS_DIVIDER));
                 }
-                mAdapterItems.add(AdapterItem.asMarketSearch(position++));
-
+                mAdapterItems.add(new AdapterItem(VIEW_TYPE_SEARCH_MARKET));
             }
         }
+        mAccessibilityResultsCount = (int) mAdapterItems.stream()
+                .filter(AdapterItem::isCountedForAccessibility).count();
+
         if (mNumAppsPerRowAllApps != 0) {
             // Update the number of rows in the adapter after we do all the merging (otherwise, we
             // would have to shift the values again)
@@ -328,19 +297,43 @@
                 }
             }
             mNumAppRowsInAdapter = rowIndex + 1;
+        }
 
-            // Pre-calculate all the fast scroller fractions
-            float perSectionTouchFraction = 1f / mFastScrollerSections.size();
-            float cumulativeTouchFraction = 0f;
-            for (FastScrollSectionInfo info : mFastScrollerSections) {
-                AdapterItem item = info.fastScrollToItem;
-                if (!BaseAllAppsAdapter.isIconViewType(item.viewType)) {
-                    info.touchFraction = 0f;
-                    continue;
-                }
-                info.touchFraction = cumulativeTouchFraction;
-                cumulativeTouchFraction += perSectionTouchFraction;
-            }
+        if (mAdapter != null) {
+            DiffUtil.calculateDiff(new MyDiffCallback(oldItems, mAdapterItems), false)
+                    .dispatchUpdatesTo(mAdapter);
         }
     }
+
+    private static class MyDiffCallback extends DiffUtil.Callback {
+
+        private final List<AdapterItem> mOldList;
+        private final List<AdapterItem> mNewList;
+
+        MyDiffCallback(List<AdapterItem> oldList, List<AdapterItem> newList) {
+            mOldList = oldList;
+            mNewList = newList;
+        }
+
+        @Override
+        public int getOldListSize() {
+            return mOldList.size();
+        }
+
+        @Override
+        public int getNewListSize() {
+            return mNewList.size();
+        }
+
+        @Override
+        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+            return mOldList.get(oldItemPosition).isSameAs(mNewList.get(newItemPosition));
+        }
+
+        @Override
+        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+            return mOldList.get(oldItemPosition).isContentSame(mNewList.get(newItemPosition));
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 8ac2536..c7c4607 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -88,10 +88,8 @@
      */
     public static class AdapterItem {
         /** Common properties */
-        // The index of this adapter item in the list
-        public int position;
         // The type of this item
-        public int viewType;
+        public final int viewType;
 
         // The row that this item shows up on
         public int rowIndex;
@@ -100,50 +98,37 @@
         // The associated ItemInfoWithIcon for the item
         public AppInfo itemInfo = null;
 
+        public AdapterItem(int viewType) {
+            this.viewType = viewType;
+        }
+
         /**
          * Factory method for AppIcon AdapterItem
          */
-        public static AdapterItem asApp(int pos, AppInfo appInfo) {
-            AdapterItem item = new AdapterItem();
-            item.viewType = VIEW_TYPE_ICON;
-            item.position = pos;
+        public static AdapterItem asApp(AppInfo appInfo) {
+            AdapterItem item = new AdapterItem(VIEW_TYPE_ICON);
             item.itemInfo = appInfo;
             return item;
         }
 
-        /**
-         * Factory method for empty search results view
-         */
-        public static AdapterItem asEmptySearch(int pos) {
-            AdapterItem item = new AdapterItem();
-            item.viewType = VIEW_TYPE_EMPTY_SEARCH;
-            item.position = pos;
-            return item;
-        }
-
-        /**
-         * Factory method for a dividerView in AllAppsSearch
-         */
-        public static AdapterItem asAllAppsDivider(int pos) {
-            AdapterItem item = new AdapterItem();
-            item.viewType = VIEW_TYPE_ALL_APPS_DIVIDER;
-            item.position = pos;
-            return item;
-        }
-
-        /**
-         * Factory method for a market search button
-         */
-        public static AdapterItem asMarketSearch(int pos) {
-            AdapterItem item = new AdapterItem();
-            item.viewType = VIEW_TYPE_SEARCH_MARKET;
-            item.position = pos;
-            return item;
-        }
-
         protected boolean isCountedForAccessibility() {
             return viewType == VIEW_TYPE_ICON || viewType == VIEW_TYPE_SEARCH_MARKET;
         }
+
+        /**
+         * Returns true if the items represent the same object
+         */
+        public boolean isSameAs(AdapterItem other) {
+            return (other.viewType != viewType) && (other.getClass() == getClass());
+        }
+
+        /**
+         * This is called only if {@link #isSameAs} returns true to check if the contents are same
+         * as well. Returning true will prevent redrawing of thee item.
+         */
+        public boolean isContentSame(AdapterItem other) {
+            return itemInfo == null && other.itemInfo == null;
+        }
     }
 
     protected final T mActivityContext;
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index 891fe8f..5bbc67e 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -142,7 +142,7 @@
         mWorkManager = new WorkProfileManager(
                 mActivityContext.getSystemService(UserManager.class),
                 this,
-                Utilities.getPrefs(mActivityContext));
+                Utilities.getPrefs(mActivityContext), mActivityContext.getDeviceProfile());
         mAH = Arrays.asList(null, null);
         mAH.set(AdapterHolder.MAIN, new AdapterHolder(false /* isWork */));
         mAH.set(AdapterHolder.WORK, new AdapterHolder(true /* isWork */));
@@ -592,13 +592,6 @@
         }
     }
 
-    /** @see View#setVerticalFadingEdgeEnabled(boolean). */
-    public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) {
-        for (int i = 0; i < mAH.size(); i++) {
-            mAH.get(i).applyVerticalFadingEdgeEnabled(enabled);
-        }
-    }
-
     public boolean isHeaderVisible() {
         return mHeader != null && mHeader.getVisibility() == View.VISIBLE;
     }
@@ -709,7 +702,6 @@
         final AlphabeticalAppsList<T> mAppsList;
         final Rect mPadding = new Rect();
         AllAppsRecyclerView mRecyclerView;
-        boolean mVerticalFadingEdge;
 
         AdapterHolder(boolean isWork) {
             mIsWork = isWork;
@@ -740,7 +732,6 @@
             FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mRecyclerView);
             mRecyclerView.addItemDecoration(focusedItemDecorator);
             adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
-            applyVerticalFadingEdgeEnabled(mVerticalFadingEdge);
             applyPadding();
         }
 
@@ -754,12 +745,6 @@
                         mPadding.bottom + bottomOffset);
             }
         }
-
-        private void applyVerticalFadingEdgeEnabled(boolean enabled) {
-            mVerticalFadingEdge = enabled;
-            mAH.get(AdapterHolder.MAIN).mRecyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
-                    && mVerticalFadingEdge);
-        }
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/WorkAdapterProvider.java b/src/com/android/launcher3/allapps/WorkAdapterProvider.java
index ce44958..76d08c8 100644
--- a/src/com/android/launcher3/allapps/WorkAdapterProvider.java
+++ b/src/com/android/launcher3/allapps/WorkAdapterProvider.java
@@ -19,10 +19,10 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
 import android.widget.TextView;
 
 import com.android.launcher3.R;
+import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
 import com.android.launcher3.model.StringCache;
 import com.android.launcher3.views.ActivityContext;
 
@@ -107,13 +107,9 @@
     public int addWorkItems(ArrayList<AllAppsGridAdapter.AdapterItem> adapterItems) {
         if (mState == WorkProfileManager.STATE_DISABLED) {
             //add disabled card here.
-            AllAppsGridAdapter.AdapterItem disabledCard = new AllAppsGridAdapter.AdapterItem();
-            disabledCard.viewType = VIEW_TYPE_WORK_DISABLED_CARD;
-            adapterItems.add(disabledCard);
+            adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_DISABLED_CARD));
         } else if (mState == WorkProfileManager.STATE_ENABLED && !isEduSeen()) {
-            AllAppsGridAdapter.AdapterItem eduCard = new AllAppsGridAdapter.AdapterItem();
-            eduCard.viewType = VIEW_TYPE_WORK_EDU_CARD;
-            adapterItems.add(eduCard);
+            adapterItems.add(new AdapterItem(VIEW_TYPE_WORK_EDU_CARD));
         }
 
         return adapterItems.size();
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index c5b02dd..dc9f18c 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -32,6 +32,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
@@ -73,15 +74,17 @@
     private final Predicate<ItemInfo> mMatcher;
 
     private WorkModeSwitch mWorkModeSwitch;
+    private final DeviceProfile mDeviceProfile;
 
     @WorkProfileState
     private int mCurrentState;
 
 
     public WorkProfileManager(UserManager userManager, BaseAllAppsContainerView<?> allApps,
-            SharedPreferences preferences) {
+            SharedPreferences preferences, DeviceProfile deviceProfile) {
         mUserManager = userManager;
         mAllApps = allApps;
+        mDeviceProfile = deviceProfile;
         mAdapterProvider = new WorkAdapterProvider(allApps.mActivityContext, preferences);
         mMatcher = mAllApps.mPersonalMatcher.negate();
     }
@@ -141,8 +144,11 @@
             mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate(
                     R.layout.work_mode_fab, mAllApps, false);
         }
+        ViewGroup.MarginLayoutParams lp =
+                (ViewGroup.MarginLayoutParams) mWorkModeSwitch.getLayoutParams();
         int workFabMarginBottom =
-                mWorkModeSwitch.getResources().getDimensionPixelSize(R.dimen.work_fab_margin);
+                mWorkModeSwitch.getResources().getDimensionPixelSize(
+                        R.dimen.work_fab_margin_bottom);
         if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
             workFabMarginBottom <<= 1;  // Double margin to add space above search bar.
             workFabMarginBottom +=
@@ -151,8 +157,11 @@
         if (!mAllApps.mActivityContext.getDeviceProfile().isGestureMode){
             workFabMarginBottom += mAllApps.mActivityContext.getDeviceProfile().getInsets().bottom;
         }
-        ((ViewGroup.MarginLayoutParams) mWorkModeSwitch.getLayoutParams()).bottomMargin =
-                workFabMarginBottom;
+        lp.bottomMargin = workFabMarginBottom;
+        int totalScreenWidth = mDeviceProfile.widthPx;
+        int personalWorkTabWidth =
+                mAllApps.mActivityContext.getAppsView().getActiveRecyclerView().getTabWidth();
+        lp.rightMargin = lp.leftMargin = (totalScreenWidth - personalWorkTabWidth) / 2;
         if (mWorkModeSwitch.getParent() != mAllApps) {
             mAllApps.addView(mWorkModeSwitch);
         }
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index bc2c318..6539c05 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -38,7 +38,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsStore;
-import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.allapps.BaseAllAppsAdapter.AdapterItem;
 import com.android.launcher3.allapps.SearchUiManager;
 import com.android.launcher3.search.SearchCallback;
@@ -57,7 +56,6 @@
     private final AllAppsSearchBarController mSearchBarController;
     private final SpannableStringBuilder mSearchQueryBuilder;
 
-    private AlphabeticalAppsList<?> mApps;
     private ActivityAllAppsContainerView<?> mAppsView;
 
     // The amount of pixels to shift down and overlap with the rest of the content.
@@ -131,7 +129,6 @@
 
     @Override
     public void initializeSearch(ActivityAllAppsContainerView<?> appsView) {
-        mApps = appsView.getApps();
         mAppsView = appsView;
         mSearchBarController.initialize(
                 new DefaultAppSearchAlgorithm(getContext()),
@@ -170,17 +167,14 @@
     @Override
     public void onSearchResult(String query, ArrayList<AdapterItem> items) {
         if (items != null) {
-            mApps.setSearchResults(items);
-            notifyResultChanged();
+            mAppsView.setSearchResults(items);
             mAppsView.setLastSearchQuery(query);
         }
     }
 
     @Override
     public void clearSearchResult() {
-        if (mApps.setSearchResults(null)) {
-            notifyResultChanged();
-        }
+        mAppsView.setSearchResults(null);
 
         // Clear the search query
         mSearchQueryBuilder.clear();
@@ -189,10 +183,6 @@
         mAppsView.onClearSearchResult();
     }
 
-    private void notifyResultChanged() {
-        mAppsView.onSearchResultsChanged();
-    }
-
     @Override
     public void setInsets(Rect insets) {
         MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
diff --git a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 33d0082..4eceb71 100644
--- a/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -85,8 +85,7 @@
         for (int i = 0; i < total && resultCount < MAX_RESULTS_COUNT; i++) {
             AppInfo info = apps.get(i);
             if (StringMatcherUtility.matches(queryTextLower, info.title.toString(), matcher)) {
-                AdapterItem appItem = AdapterItem.asApp(resultCount, info);
-                result.add(appItem);
+                result.add(AdapterItem.asApp(info));
                 resultCount++;
             }
         }
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 4ff5d5e..f8a2c79 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -156,14 +156,18 @@
                     String.format("upperBound (%f) must be greater than lowerBound (%f)",
                             upperBound, lowerBound));
         }
-        return t -> clampToProgress(t, lowerBound, upperBound);
+        return t -> clampToProgress(interpolator, t, lowerBound, upperBound);
     }
 
     /**
      * Returns the progress value's progress between the lower and upper bounds. That is, the
      * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
+     *
+     * Between lowerBound and upperBound, the progress value will be interpolated using the provided
+     * interpolator.
      */
-    public static float clampToProgress(float progress, float lowerBound, float upperBound) {
+    public static float clampToProgress(
+            Interpolator interpolator, float progress, float lowerBound, float upperBound) {
         if (upperBound < lowerBound) {
             throw new IllegalArgumentException(
                     String.format("upperBound (%f) must be greater than lowerBound (%f)",
@@ -179,7 +183,15 @@
         if (progress > upperBound) {
             return 1;
         }
-        return (progress - lowerBound) / (upperBound - lowerBound);
+        return interpolator.getInterpolation((progress - lowerBound) / (upperBound - lowerBound));
+    }
+
+    /**
+     * Returns the progress value's progress between the lower and upper bounds. That is, the
+     * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
+     */
+    public static float clampToProgress(float progress, float lowerBound, float upperBound) {
+        return clampToProgress(Interpolators.LINEAR, progress, lowerBound, upperBound);
     }
 
     /**
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index a11bd4f..d5bcb0c 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -83,6 +83,7 @@
 import com.android.launcher3.uioverrides.PredictedAppIconInflater;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
@@ -129,7 +130,7 @@
             super(base, UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
                     LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                     CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE,
-                    WindowManagerProxy.INSTANCE);
+                    WindowManagerProxy.INSTANCE, DisplayController.INSTANCE);
             mIdp = idp;
             mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
             mObjectMap.put(LauncherAppState.INSTANCE,
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index e36d4cf..ef9250c 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -216,14 +216,15 @@
             return false;
         }
 
+        // Sort the items by the reading order.
+        Collections.sort(mHotseatDiff);
+        Collections.sort(mWorkspaceDiff);
+
         // Migrate hotseat
         HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader,
                 mDestReader, mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff);
         hotseatSolution.find();
 
-        // Sort the items by the reading order.
-        Collections.sort(mWorkspaceDiff);
-
         // Migrate workspace.
         // First we create a collection of the screens
         List<Integer> screens = new ArrayList<>();
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index 4a68dda..f9a36ad 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.DeviceProfile.DeviceProfileListenable;
 
 /**
  * Interface representing a state of a StatefulActivity
@@ -36,7 +37,8 @@
     /**
      * @return How long the animation to this state should take (or from this state to NORMAL).
      */
-    int getTransitionDuration(Context context, boolean isToState);
+    <DEVICE_PROFILE_CONTEXT extends Context & DeviceProfileListenable>
+    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState);
 
     /**
      * Returns the state to go back to from this state
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 15cdc20..a205ab5 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -51,7 +51,7 @@
             return super.getWorkspaceScaleAndTranslation(launcher);
         }
 
-        float shrunkTop = grid.getWorkspaceSpringLoadShrunkTop();
+        float shrunkTop = grid.getCellLayoutSpringLoadShrunkTop();
         float scale = grid.getWorkspaceSpringLoadScale();
 
         float halfHeight = ws.getHeight() / 2;
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index a6b481a..242d2d4 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -198,6 +198,12 @@
                     return new Point(cellRect.centerX(), cellRect.centerY());
                 });
 
+            case TestProtocol.REQUEST_HAS_TIS: {
+                response.putBoolean(
+                        TestProtocol.REQUEST_HAS_TIS, false);
+                return response;
+            }
+
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index e8fd2ff..3a030a8 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -110,6 +110,7 @@
     public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT =
             "get-activities-created-count";
     public static final String REQUEST_GET_ACTIVITIES = "get-activities";
+    public static final String REQUEST_HAS_TIS = "has-touch-interaction-service";
 
     public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
     public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index a125fbe..09b8228 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.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.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -330,9 +331,6 @@
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
-        if (targetState != mStartState) {
-            logReachedState(targetState);
-        }
         mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState));
         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
         anim.setFloatValues(startProgress, endProgress);
@@ -361,6 +359,8 @@
         boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
         if (shouldGoToTargetState) {
             goToTargetState(targetState);
+        } else {
+            logReachedState(mToState);
         }
     }
 
@@ -368,13 +368,19 @@
         if (!mLauncher.isInState(targetState)) {
             // If we're already in the target state, don't jump to it at the end of the animation in
             // case the user started interacting with it before the animation finished.
-            mLauncher.getStateManager().goToState(targetState, false /* animated */);
+            mLauncher.getStateManager().goToState(targetState, false /* animated */,
+                    forEndCallback(() -> logReachedState(targetState)));
+        } else {
+            logReachedState(targetState);
         }
         mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(
                 1f).setDuration(0).start();
     }
 
     private void logReachedState(LauncherState targetState) {
+        if (mStartState == targetState) {
+            return;
+        }
         // Transition complete. log the action
         mLauncher.getStatsLogManager().logger()
                 .withSrcState(mStartState.statsLogOrdinal)
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index b0e2ec1..48df04f 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -95,7 +95,7 @@
         mTitle = findViewById(R.id.app_title);
         mSubtitle = findViewById(R.id.app_subtitle);
         mExpandToggle = findViewById(R.id.toggle);
-        findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() {
+        setAccessibilityDelegate(new AccessibilityDelegate() {
 
             @Override
             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index f780f03..755e4a9 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -239,21 +239,6 @@
         mHeaderViewDimensionsProvider = headerViewDimensionsProvider;
     }
 
-    @Override
-    public void scrollToTop() {
-        if (mScrollbar != null) {
-            mScrollbar.reattachThumbToScroll();
-        }
-
-        if (getLayoutManager() instanceof LinearLayoutManager) {
-            if (getCurrentScrollY() == 0) {
-                // We are at the top, so don't scrollToPosition (would cause unnecessary relayout).
-                return;
-            }
-        }
-        scrollToPosition(0);
-    }
-
     /**
      * Returns the sum of the height, in pixels, of this list adapter's items from index 0 until
      * {@code untilIndex}.
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
index 2f8e680..52a6759 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -20,6 +20,7 @@
 
 import android.content.Context;
 
+import com.android.launcher3.DeviceProfile.DeviceProfileListenable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
@@ -39,8 +40,11 @@
     }
 
     @Override
-    public int getTransitionDuration(Context context, boolean isToState) {
-        return isToState ? 500 : 300;
+    public <DEVICE_PROFILE_CONTEXT extends Context & DeviceProfileListenable>
+    int getTransitionDuration(DEVICE_PROFILE_CONTEXT context, boolean isToState) {
+        return !context.getDeviceProfile().isTablet && isToState
+                ? 600
+                : isToState ? 500 : 300;
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt b/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt
new file mode 100644
index 0000000..63abc7d
--- /dev/null
+++ b/tests/src/com/android/launcher3/DeviceProfileGridDimensionsTest.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2022 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
+
+import android.graphics.PointF
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.WindowBounds
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.`when` as whenever
+
+/**
+ * Test for [DeviceProfile] grid dimensions.
+ *
+ * This includes workspace, cell layout, shortcut and widget container, cell sizes, etc.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceProfileGridDimensionsTest : DeviceProfileBaseTest() {
+
+    @Test
+    fun getWorkspaceWidth_twoPanelLandscapeScalable4By4GridTablet_workspaceWidthIsFullPage() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceWidth = availableWidth
+        assertThat(dp.workspaceWidth).isEqualTo(expectedWorkspaceWidth)
+    }
+
+    @Test
+    fun getWorkspaceHeight_twoPanelLandscapeScalable4By4GridTablet_workspaceHeightIsFullPage() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceHeight = availableHeight
+        assertThat(dp.workspaceHeight).isEqualTo(expectedWorkspaceHeight)
+    }
+
+    @Test
+    fun getCellLayoutWidth_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelWidth() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceWidth = availableWidth
+        val expectedCellLayoutWidth =
+                (expectedWorkspaceWidth - (dp.workspacePadding.right + dp.workspacePadding.left)) /
+                        dp.panelCount
+        assertThat(dp.cellLayoutWidth).isEqualTo(expectedCellLayoutWidth)
+    }
+
+    @Test
+    fun getCellLayoutHeight_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelHeight() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceHeight = availableHeight
+        val expectedCellLayoutHeight =
+                expectedWorkspaceHeight - (dp.workspacePadding.top + dp.workspacePadding.bottom)
+        assertThat(dp.cellLayoutHeight).isEqualTo(expectedCellLayoutHeight)
+    }
+
+    @Test
+    fun getShortcutAndWidgetContainerWidth_twoPanelLandscapeScalable4By4GridTablet_equalsIconsPlusBorderSpacesWidth() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceWidth = availableWidth
+        val expectedCellLayoutWidth =
+                (expectedWorkspaceWidth - (dp.workspacePadding.right + dp.workspacePadding.left)) /
+                        dp.panelCount
+        val expectedShortcutAndWidgetContainerWidth = expectedCellLayoutWidth -
+                (dp.cellLayoutPaddingPx.left + dp.cellLayoutPaddingPx.right)
+        assertThat(dp.shortcutAndWidgetContainerWidth).isEqualTo(expectedShortcutAndWidgetContainerWidth)
+    }
+
+    @Test
+    fun getShortcutAndWidgetContainerHeight_twoPanelLandscapeScalable4By4GridTablet_equalsIconsPlusBorderSpacesHeight() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceHeight = availableHeight
+        val expectedCellLayoutHeight =
+                expectedWorkspaceHeight - (dp.workspacePadding.top + dp.workspacePadding.bottom)
+        val expectedShortcutAndWidgetContainerHeight = expectedCellLayoutHeight -
+                (dp.cellLayoutPaddingPx.top + dp.cellLayoutPaddingPx.bottom)
+        assertThat(dp.shortcutAndWidgetContainerHeight).isEqualTo(
+                expectedShortcutAndWidgetContainerHeight)
+    }
+
+    @Test
+    fun getCellSize_twoPanelLandscapeScalable4By4GridTablet_equalsSinglePanelWidth() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        val expectedWorkspaceWidth = availableWidth
+        val expectedCellLayoutWidth =
+                (expectedWorkspaceWidth - (dp.workspacePadding.right + dp.workspacePadding.left)) /
+                        dp.panelCount
+        val expectedShortcutAndWidgetContainerWidth =
+                expectedCellLayoutWidth -
+                        (dp.cellLayoutPaddingPx.left + dp.cellLayoutPaddingPx.right)
+        assertThat(dp.getCellSize().x).isEqualTo(
+                (expectedShortcutAndWidgetContainerWidth -
+                        ((inv!!.numColumns - 1) * dp.cellLayoutBorderSpacePx.x)) / inv!!.numColumns)
+        val expectedWorkspaceHeight = availableHeight
+        val expectedCellLayoutHeight =
+                expectedWorkspaceHeight - (dp.workspacePadding.top + dp.workspacePadding.bottom)
+        val expectedShortcutAndWidgetContainerHeight = expectedCellLayoutHeight -
+                (dp.cellLayoutPaddingPx.top + dp.cellLayoutPaddingPx.bottom)
+        assertThat(dp.getCellSize().y).isEqualTo(
+                (expectedShortcutAndWidgetContainerHeight -
+                        ((inv!!.numRows - 1) * dp.cellLayoutBorderSpacePx.y)) / inv!!.numRows)
+    }
+
+    @Test
+    fun getPanelCount_twoPanelLandscapeScalable4By4GridTablet_equalsTwoPanels() {
+        val tabletWidth = 2560
+        val tabletHeight = 1600
+        val availableWidth = 2560
+        val availableHeight = 1500
+        windowBounds = WindowBounds(tabletWidth, tabletHeight, availableWidth, availableHeight, 0)
+        useTwoPanels = true
+        whenever(info.isTablet(ArgumentMatchers.any())).thenReturn(true)
+        whenever(info.getDensityDpi()).thenReturn(320)
+        inv = getScalable4By4InvariantDeviceProfile()
+
+        val dp = newDP()
+
+        assertThat(dp.panelCount).isEqualTo(2)
+    }
+
+    fun getScalable4By4InvariantDeviceProfile(): InvariantDeviceProfile {
+        return InvariantDeviceProfile().apply {
+            isScalable = true
+            numColumns = 4
+            numRows = 4
+            numShownHotseatIcons = 4
+            numDatabaseHotseatIcons = 6
+            numShrunkenHotseatIcons = 5
+            horizontalMargin = FloatArray(4) { 22f }
+            borderSpaces = listOf(
+                    PointF(16f, 16f),
+                    PointF(16f, 16f),
+                    PointF(16f, 16f),
+                    PointF(16f, 16f)
+            ).toTypedArray()
+            allAppsBorderSpaces = listOf(
+                    PointF(16f, 16f),
+                    PointF(16f, 16f),
+                    PointF(16f, 16f),
+                    PointF(16f, 16f)
+            ).toTypedArray()
+            hotseatBorderSpaces = FloatArray(4) { 16f }
+            hotseatColumnSpan = IntArray(4) { 4 }
+            iconSize = FloatArray(4) { 56f }
+            allAppsIconSize = FloatArray(4) { 56f }
+            iconTextSize = FloatArray(4) { 14f }
+            allAppsIconTextSize = FloatArray(4) { 14f }
+            minCellSize = listOf(
+                    PointF(64f, 83f),
+                    PointF(64f, 83f),
+                    PointF(64f, 83f),
+                    PointF(64f, 83f)
+            ).toTypedArray()
+            allAppsCellSize = listOf(
+                    PointF(64f, 83f),
+                    PointF(64f, 83f),
+                    PointF(64f, 83f),
+                    PointF(64f, 83f)
+            ).toTypedArray()
+            inlineQsb = booleanArrayOf(
+                    false,
+                    false,
+                    false,
+                    false
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 7080c85..6f8b9d2 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -613,6 +613,10 @@
         return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
     }
 
+    protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
+        return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
+    }
+
     protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
         HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
         if (homeAppIcon == null) {
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index f0bef24..15e8f68 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,8 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -54,11 +56,16 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
+import java.util.Map;
+
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
     private static final String APP_NAME = "LauncherTestApp";
     private static final String DUMMY_APP_NAME = "Aardwolf";
+    private static final String MAPS_APP_NAME = "Maps";
+    private static final String STORE_APP_NAME = "Play Store";
 
     @Before
     public void setUp() throws Exception {
@@ -462,15 +469,7 @@
     @Test
     @PortraitLandscape
     public void testDragAppIconToWorkspaceCell() throws Exception {
-        final Point dimensions = mLauncher.getWorkspace().getIconGridDimensions();
-
-        Point[] targets = {
-                new Point(0, 1),
-                new Point(0, dimensions.y - 2),
-                new Point(dimensions.x - 1, 1),
-                new Point(dimensions.x - 1, dimensions.y - 2),
-                new Point(dimensions.x / 2, dimensions.y / 2)
-        };
+        Point[] targets = getCornersAndCenterPositions();
 
         for (Point target : targets) {
             final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
@@ -491,6 +490,48 @@
         }
     }
 
+    @Test
+    public void getIconsPosition_afterIconRemoved_notContained() throws IOException {
+        Point[] gridPositions = getCornersAndCenterPositions();
+        createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
+        createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
+        TestUtil.installDummyApp();
+        try {
+            createShortcutIfNotExist(DUMMY_APP_NAME, gridPositions[2]);
+            Map<String, Point> initialPositions =
+                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
+            assertThat(initialPositions.keySet())
+                    .containsAtLeast(DUMMY_APP_NAME, MAPS_APP_NAME, STORE_APP_NAME);
+
+            mLauncher.getWorkspace().getWorkspaceAppIcon(DUMMY_APP_NAME).uninstall();
+
+            assertNull(
+                    DUMMY_APP_NAME + " app was found after being uninstalled",
+                    mLauncher.getWorkspace().tryGetWorkspaceAppIcon(DUMMY_APP_NAME));
+
+            Map<String, Point> finalPositions =
+                    mLauncher.getWorkspace().getWorkspaceIconsPositions();
+            assertThat(finalPositions).doesNotContainKey(DUMMY_APP_NAME);
+        } finally {
+            TestUtil.uninstallDummyApp();
+        }
+    }
+
+    /**
+     * @return List of workspace grid coordinates. Those are not pixels. See {@link
+     *     Workspace#getIconGridDimensions()}
+     */
+    private Point[] getCornersAndCenterPositions() {
+        final Point dimensions = mLauncher.getWorkspace().getIconGridDimensions();
+        return new Point[] {
+            new Point(0, 1),
+            new Point(0, dimensions.y - 2),
+            new Point(dimensions.x - 1, 1),
+            new Point(dimensions.x - 1, dimensions.y - 2),
+            new Point(dimensions.x / 2, dimensions.y / 2)
+        };
+    }
+
     public static String getAppPackageName() {
         return getInstrumentation().getContext().getPackageName();
     }
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index f31e4f3..7c1be1d 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -25,11 +25,14 @@
 import android.util.Log;
 import android.view.View;
 
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsPagedView;
 import com.android.launcher3.allapps.WorkAdapterProvider;
 import com.android.launcher3.allapps.WorkEduCard;
+import com.android.launcher3.allapps.WorkPausedCard;
 import com.android.launcher3.allapps.WorkProfileManager;
 import com.android.launcher3.tapl.LauncherInstrumentation;
 
@@ -38,6 +41,7 @@
 import org.junit.Test;
 
 import java.util.Objects;
+import java.util.function.Predicate;
 
 public class WorkProfileTest extends AbstractLauncherUiTest {
 
@@ -130,6 +134,8 @@
             return manager.getCurrentState() == WorkProfileManager.STATE_DISABLED;
         }, LauncherInstrumentation.WAIT_TIME_MS);
 
+        waitForWorkCard("Work paused card not shown", view -> view instanceof WorkPausedCard);
+
         // start work profile toggle ON test
         executeOnLauncher(l -> {
             ActivityAllAppsContainerView<?> allApps = l.getAppsView();
@@ -154,9 +160,19 @@
             l.getAppsView().getWorkManager().reset();
         });
 
-        waitForLauncherCondition("Work profile education not shown",
-                l -> l.getAppsView().getActiveRecyclerView()
-                        .findViewHolderForAdapterPosition(0).itemView instanceof WorkEduCard,
-                LauncherInstrumentation.WAIT_TIME_MS);
+        waitForWorkCard("Work profile education not shown", view -> view instanceof WorkEduCard);
+    }
+
+    private void waitForWorkCard(String message, Predicate<View> workCardCheck) {
+        waitForLauncherCondition(message, l -> {
+            l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+            ViewHolder holder = l.getAppsView().getActiveRecyclerView()
+                    .findViewHolderForAdapterPosition(0);
+            try {
+                return holder != null && workCardCheck.test(holder.itemView);
+            } finally {
+                l.getAppsView().getAppsStore().enableDeferUpdates(DEFER_UPDATES_TEST);
+            }
+        }, LauncherInstrumentation.WAIT_TIME_MS);
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
new file mode 100644
index 0000000..5f92199
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+/**
+ * Operations on home screen qsb.
+ */
+public class HomeQsb {
+
+    private final LauncherInstrumentation mLauncher;
+
+    HomeQsb(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mLauncher.waitForLauncherObject("search_container_hotseat");
+    }
+
+    /**
+     * Show search result page from tapping qsb.
+     */
+    public SearchResultFromQsb showSearchResult() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to open search result page");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.clickLauncherObject(
+                    mLauncher.waitForLauncherObject("search_container_hotseat"));
+            try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                    "clicked qsb to open search result page")) {
+                return new SearchResultFromQsb(mLauncher);
+            }
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 45a0196..39cd4ff 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -33,7 +33,7 @@
 /**
  * Ancestor for AppIcon and AppMenuItem.
  */
-abstract class Launchable {
+public abstract class Launchable {
 
     protected static final int DEFAULT_DRAG_STEPS = 10;
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index afb4f8d..2b3583e 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1499,9 +1499,14 @@
                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
     }
 
+    private boolean hasTIS() {
+        return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(TestProtocol.REQUEST_HAS_TIS);
+    }
+
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope) {
-        final boolean notLauncher3 = !isLauncher3();
+        final boolean hasTIS = hasTIS();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
@@ -1509,12 +1514,12 @@
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
                 }
-                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
                 }
                 break;
             case MotionEvent.ACTION_UP:
-                if (notLauncher3 && gestureScope != GestureScope.INSIDE
+                if (hasTIS && gestureScope != GestureScope.INSIDE
                         && gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
                         && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
                         || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
@@ -1528,7 +1533,7 @@
                                     || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
                                     ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
                 }
-                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS,
                             gestureScope == GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
                                     || gestureScope == GestureScope.OUTSIDE_WITH_KEYCODE
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
new file mode 100644
index 0000000..82652c7
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import android.widget.TextView;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * Operations on search result page opened from home screen qsb.
+ */
+public class SearchResultFromQsb {
+    // The input resource id in the search box.
+    private static final String INPUT_RES = "input";
+    private final LauncherInstrumentation mLauncher;
+
+    SearchResultFromQsb(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mLauncher.waitForLauncherObject("search_container_all_apps");
+    }
+
+    /** Set the input to the search input edit text and update search results. */
+    public void searchForInput(String input) {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to search for result with an input");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.waitForLauncherObject(INPUT_RES).setText(input);
+        }
+    }
+
+    /** Find the app from search results with app name. */
+    public Launchable findAppIcon(String appName) {
+        UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName));
+        return new AllAppsAppIcon(mLauncher, icon);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index ae0f6a6..954af3d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -43,6 +43,7 @@
 import com.android.launcher3.testing.WorkspaceCellCenterRequest;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -111,6 +112,18 @@
     }
 
     /**
+     * Returns the home qsb.
+     *
+     * The qsb must already be visible when calling this method.
+     */
+    public HomeQsb getQsb() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get the home qsb")) {
+            return new HomeQsb(mLauncher);
+        }
+    }
+
+    /**
      * Returns an icon for the app, if currently visible.
      *
      * @param appName name of the app
@@ -222,6 +235,21 @@
                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
     }
 
+    /**
+     * @return map of text -> center of the view. In case of icons with the same name, the one with
+     *     lower x coordinate is selected.
+     */
+    public Map<String, Point> getWorkspaceIconsPositions() {
+        final UiObject2 workspace = verifyActiveContainer();
+        List<UiObject2> workspaceIcons =
+                mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector());
+        return workspaceIcons.stream()
+                .collect(
+                        Collectors.toMap(
+                                /* keyMapper= */ UiObject2::getText,
+                                /* valueMapper= */ UiObject2::getVisibleCenter,
+                                /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2));
+    }
     /*
      * Get the center point of the delete/uninstall icon in the drop target bar.
      */