Merge "Import translations. DO NOT MERGE ANYWHERE" into udc-dev
diff --git a/Android.bp b/Android.bp
index e730c9d..a7edf2a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -79,7 +79,7 @@
         "androidx.test.uiautomator_uiautomator",
         "androidx.preference_preference",
         "SystemUISharedLib",
-        "SystemUIAnimationLib",
+        "animationlib",
         "launcher-testing-shared",
     ],
     srcs: [
@@ -243,7 +243,7 @@
         "lottie",
         "SystemUISharedLib",
         "SystemUI-statsd",
-        "SystemUIAnimationLib",
+        "animationlib",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     min_sdk_version: "current",
@@ -305,7 +305,7 @@
         "SystemUISharedLib",
         "Launcher3CommonDepsLib",
         "QuickstepResLib",
-        "SystemUIAnimationLib",
+        "animationlib",
     ],
     manifest: "quickstep/AndroidManifest.xml",
     platform_apis: true,
diff --git a/res/drawable/taskbar_divider_bg.xml b/quickstep/res/drawable/taskbar_divider_bg.xml
similarity index 85%
rename from res/drawable/taskbar_divider_bg.xml
rename to quickstep/res/drawable/taskbar_divider_bg.xml
index a8c2ae7..52e230d 100644
--- a/res/drawable/taskbar_divider_bg.xml
+++ b/quickstep/res/drawable/taskbar_divider_bg.xml
@@ -14,8 +14,7 @@
      limitations under the License.
 -->
 <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"/>
+    <solid android:color="@color/taskbar_divider_background"/>
     <corners android:radius="1dp" />
 </shape>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
index 7dda73f..7f65e41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarEduTooltip.kt
@@ -26,15 +26,15 @@
 import android.view.View
 import android.view.ViewGroup
 import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.app.animation.Interpolators.STANDARD
 import com.android.launcher3.AbstractFloatingView
 import com.android.launcher3.R
 import com.android.launcher3.anim.AnimatorListeners
 import com.android.launcher3.popup.RoundedArrowDrawable
 import com.android.launcher3.util.Themes
 import com.android.launcher3.views.ActivityContext
-import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
-import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
-import com.android.systemui.animation.Interpolators.STANDARD
 
 private const val ENTER_DURATION_MS = 300L
 private const val EXIT_DURATION_MS = 150L
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 59b5e74..ed78e2d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -15,13 +15,13 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.app.animation.Interpolators.EMPHASIZED;
 import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP;
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE;
 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
 import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
 import static com.android.launcher3.util.FlagDebugUtils.formatFlagChange;
-import static com.android.systemui.animation.Interpolators.EMPHASIZED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index f099e06..bc1a2c8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -515,6 +515,14 @@
     }
 
     /**
+     * Returns the taskbar divider in the taskbar.
+     */
+    @Nullable
+    public View getTaskbarDividerView() {
+        return mTaskbarDivider;
+    }
+
+    /**
      * Returns the QSB in the taskbar.
      */
     public View getQsb() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index ec3d1bc..a7e2daa 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -459,12 +459,14 @@
         for (int i = 0; i < mTaskbarView.getChildCount(); i++) {
             View child = mTaskbarView.getChildAt(i);
             boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonView();
+            boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerView();
             if (!mIsHotseatIconOnTopWhenAligned) {
                 // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController
                 // plays iconAlignment to 1 really fast, therefore moving the fading towards the end
                 // to avoid icons disappearing rather than fading out visually.
                 setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f));
-            } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get())) {
+            } else if ((isAllAppsButton && !FeatureFlags.ENABLE_ALL_APPS_BUTTON_IN_HOTSEAT.get())
+                    || (isTaskbarDividerView && FeatureFlags.ENABLE_TASKBAR_PINNING.get())) {
                 if (!isToHome
                         && mIsHotseatIconOnTopWhenAligned
                         && mControllers.taskbarStashController.isStashed()) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 549c50b..65f449c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1063,7 +1063,8 @@
     }
 
     @Override
-    public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+    public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+            int workspaceItemCount, boolean isBindSync) {
         pendingTasks.add(() -> {
             // This is added in pending task as we need to wait for views to be positioned
             // correctly before registering them for the animation.
@@ -1073,7 +1074,7 @@
                 mLauncherUnfoldAnimationController.updateRegisteredViewsIfNeeded();
             }
         });
-        super.onInitialBindComplete(boundPages, pendingTasks);
+        super.onInitialBindComplete(boundPages, pendingTasks, workspaceItemCount, isBindSync);
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
index 36e78fb..39543b0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepWidgetHolder.java
@@ -95,7 +95,11 @@
                     i -> MAIN_EXECUTOR.execute(() ->
                             sHolders.forEach(h -> h.mAppWidgetRemovedCallback.accept(i))),
                     () -> MAIN_EXECUTOR.execute(() ->
-                            sHolders.forEach(h -> h.mProviderChangedListeners.forEach(
+                            sHolders.forEach(h ->
+                                    // Listeners might remove themselves from the list during the
+                                    // iteration. Creating a copy of the list to avoid exceptions
+                                    // for concurrent modification.
+                                    new ArrayList<>(h.mProviderChangedListeners).forEach(
                                     ProviderChangedListener::notifyWidgetProvidersChanged))),
                     UI_HELPER_EXECUTOR.getLooper());
             if (!WidgetsModel.GO_DISABLE_WIDGETS) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
index 89aba90..e1ce9b1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
@@ -359,17 +359,6 @@
             return true;
         });
         sandboxCategory.addPreference(launchOverviewTutorialPreference);
-        Preference launchAssistantTutorialPreference = new Preference(context);
-        launchAssistantTutorialPreference.setKey("launchAssistantTutorial");
-        launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial");
-        launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture");
-        launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> {
-            startActivity(launchSandboxIntent
-                    .putExtra("use_tutorial_menu", false)
-                    .putExtra("tutorial_steps", new String[] {"ASSISTANT"}));
-            return true;
-        });
-        sandboxCategory.addPreference(launchAssistantTutorialPreference);
         Preference launchSandboxModeTutorialPreference = new Preference(context);
         launchSandboxModeTutorialPreference.setKey("launchSandboxMode");
         launchSandboxModeTutorialPreference.setTitle("Launch Sandbox Mode");
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
index cd98e7a..64165b6 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OverviewInputConsumer.java
@@ -61,14 +61,6 @@
 
         mTarget = activity.getDragLayer();
         mTarget.getLocationOnScreen(mLocationOnScreen);
-
-        // When Overview is launched via meta+tab or swipe up from an app,
-        // the touch mode somehow is not changed to false by the Android framework.
-        // The subsequent key events (e.g. DPAD_LEFT, DPAD_RIGHT) can only be dispatched to
-        // focused views, while focus can only be requested in
-        // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note,
-        // here we launch overview with live tile.
-        mActivity.getRootView().getViewRootImpl().touchModeChanged(false);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
deleted file mode 100644
index 40c600f..0000000
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialController.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.graphics.PointF;
-
-import com.android.launcher3.R;
-import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
-import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
-
-/** A {@link TutorialController} for the Assistant tutorial. */
-final class AssistantGestureTutorialController extends TutorialController {
-
-    AssistantGestureTutorialController(AssistantGestureTutorialFragment fragment,
-                                       TutorialType tutorialType) {
-        super(fragment, tutorialType);
-    }
-
-    @Override
-    public void onBackGestureAttempted(BackGestureResult result) {
-        switch (mTutorialType) {
-            case ASSISTANT:
-                switch (result) {
-                    case BACK_COMPLETED_FROM_LEFT:
-                    case BACK_COMPLETED_FROM_RIGHT:
-                    case BACK_CANCELLED_FROM_LEFT:
-                    case BACK_CANCELLED_FROM_RIGHT:
-                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
-                        break;
-                }
-                break;
-            case ASSISTANT_COMPLETE:
-                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
-                        || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
-                    mTutorialFragment.close();
-                }
-                break;
-        }
-    }
-
-
-    @Override
-    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
-        switch (mTutorialType) {
-            case ASSISTANT:
-                switch (result) {
-                    case HOME_GESTURE_COMPLETED:
-                    case OVERVIEW_GESTURE_COMPLETED:
-                    case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
-                    case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
-                    case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
-                    case HOME_OR_OVERVIEW_CANCELLED:
-                        showFeedback(R.string.assistant_gesture_feedback_swipe_too_far_from_corner);
-                        break;
-                    case ASSISTANT_COMPLETED:
-                        showRippleEffect(null);
-                        showFeedback(R.string.assistant_gesture_tutorial_playground_subtitle);
-                        break;
-                    case ASSISTANT_NOT_STARTED_BAD_ANGLE:
-                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_diagonal);
-                        break;
-                    case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT:
-                        showFeedback(R.string.assistant_gesture_feedback_swipe_not_long_enough);
-                        break;
-                }
-                break;
-            case ASSISTANT_COMPLETE:
-                if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
-                    mTutorialFragment.close();
-                }
-                break;
-        }
-    }
-
-    @Override
-    public void setAssistantProgress(float progress) {
-        // TODO: Create an animation.
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
deleted file mode 100644
index 90a1c36..0000000
--- a/quickstep/src/com/android/quickstep/interaction/AssistantGestureTutorialFragment.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.interaction;
-
-import android.view.MotionEvent;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.logging.StatsLogManager;
-import com.android.quickstep.interaction.TutorialController.TutorialType;
-
-/** Shows the Home gesture interactive tutorial. */
-public class AssistantGestureTutorialFragment extends TutorialFragment {
-
-    public AssistantGestureTutorialFragment(boolean fromTutorialMenu) {
-        super(fromTutorialMenu);
-    }
-
-    @Override
-    TutorialController createController(TutorialType type) {
-        return new AssistantGestureTutorialController(this, type);
-    }
-
-    @Override
-    Class<? extends TutorialController> getControllerClass() {
-        return AssistantGestureTutorialController.class;
-    }
-
-    @Override
-    public boolean onTouch(View view, MotionEvent motionEvent) {
-        if (motionEvent.getAction() == MotionEvent.ACTION_DOWN && mTutorialController != null) {
-            mTutorialController.setRippleHotspot(motionEvent.getX(), motionEvent.getY());
-        }
-        return super.onTouch(view, motionEvent);
-    }
-
-    @Override
-    void logTutorialStepShown(@NonNull StatsLogManager statsLogManager) {
-        // No-Op: tutorial step not currently shown to users
-    }
-
-    @Override
-    void logTutorialStepCompleted(@NonNull StatsLogManager statsLogManager) {
-        // No-Op: tutorial step not currently shown to users
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index e1f0924..ab76fb4 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -205,9 +205,6 @@
                 case HOME_GESTURE_COMPLETED:
                 case OVERVIEW_GESTURE_COMPLETED:
                 case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
-                case ASSISTANT_COMPLETED:
-                case ASSISTANT_NOT_STARTED_BAD_ANGLE:
-                case ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT:
                 default:
                     showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
 
diff --git a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
index 57874d9..6cee690 100644
--- a/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
+++ b/quickstep/src/com/android/quickstep/interaction/NavBarGestureHandler.java
@@ -15,11 +15,7 @@
  */
 package com.android.quickstep.interaction;
 
-import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
-import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_COMPLETED;
-import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_BAD_ANGLE;
-import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
@@ -27,24 +23,16 @@
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
 import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
 
-import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.PointF;
-import android.graphics.RectF;
-import android.os.SystemClock;
-import android.view.Display;
-import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnTouchListener;
-import android.view.ViewConfiguration;
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.R;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.testing.shared.ResourceUtils;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.NavigationMode;
@@ -52,46 +40,25 @@
 import com.android.quickstep.util.MotionPauseDetector;
 import com.android.quickstep.util.NavBarPosition;
 import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
-import com.android.systemui.shared.system.QuickStepContract;
 
-/** Utility class to handle Home and Assistant gestures. */
+/** Utility class to handle Home gesture. */
 public class NavBarGestureHandler implements OnTouchListener,
         TriggerSwipeUpTouchTracker.OnSwipeUpListener, MotionPauseDetector.OnMotionPauseListener {
 
     private static final String LOG_TAG = "NavBarGestureHandler";
-    private static final long RETRACT_GESTURE_ANIMATION_DURATION_MS = 300;
-
     private final Context mContext;
     private final Point mDisplaySize = new Point();
     private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
     private final int mBottomGestureHeight;
-    private final GestureDetector mAssistantGestureDetector;
-    private final int mAssistantAngleThreshold;
-    private final RectF mAssistantLeftRegion = new RectF();
-    private final RectF mAssistantRightRegion = new RectF();
-    private final float mAssistantDragDistThreshold;
-    private final float mAssistantFlingDistThreshold;
-    private final long mAssistantTimeThreshold;
-    private final float mAssistantSquaredSlop;
-    private final PointF mAssistantStartDragPos = new PointF();
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private final MotionPauseDetector mMotionPauseDetector;
-    private boolean mTouchCameFromAssistantCorner;
     private boolean mTouchCameFromNavBar;
-    private boolean mPassedAssistantSlop;
-    private boolean mAssistantGestureActive;
-    private boolean mLaunchedAssistant;
-    private long mAssistantDragStartTime;
-    private float mAssistantDistance;
-    private float mAssistantTimeFraction;
-    private float mAssistantLastProgress;
     @Nullable
     private NavBarGestureAttemptCallback mGestureCallback;
 
     NavBarGestureHandler(Context context) {
         mContext = context;
-        final Display display = mContext.getDisplay();
         DisplayController.Info displayInfo = DisplayController.INSTANCE.get(mContext).getInfo();
         final int displayRotation = displayInfo.rotation;
         Point currentSize = displayInfo.currentSize;
@@ -105,27 +72,6 @@
         final Resources resources = context.getResources();
         mBottomGestureHeight =
                 ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, resources);
-        mAssistantDragDistThreshold =
-                resources.getDimension(R.dimen.gestures_assistant_drag_threshold);
-        mAssistantFlingDistThreshold =
-                resources.getDimension(R.dimen.gestures_assistant_fling_threshold);
-        mAssistantTimeThreshold =
-                resources.getInteger(R.integer.assistant_gesture_min_time_threshold);
-        mAssistantAngleThreshold =
-                resources.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
-
-        mAssistantGestureDetector = new GestureDetector(context, new AssistantGestureListener());
-        int assistantWidth = resources.getDimensionPixelSize(R.dimen.gestures_assistant_width);
-        final float assistantHeight = Math.max(mBottomGestureHeight,
-                QuickStepContract.getWindowCornerRadius(context));
-        mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = mDisplaySize.y;
-        mAssistantLeftRegion.top = mAssistantRightRegion.top = mDisplaySize.y - assistantHeight;
-        mAssistantLeftRegion.left = 0;
-        mAssistantLeftRegion.right = assistantWidth;
-        mAssistantRightRegion.right = mDisplaySize.x;
-        mAssistantRightRegion.left = mDisplaySize.x - assistantWidth;
-        float slop = ViewConfiguration.get(context).getScaledTouchSlop();
-        mAssistantSquaredSlop = slop * slop;
     }
 
     void registerNavBarGestureAttemptCallback(NavBarGestureAttemptCallback callback) {
@@ -138,7 +84,7 @@
 
     @Override
     public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
-        if (mGestureCallback == null || mAssistantGestureActive) {
+        if (mGestureCallback == null) {
             return;
         }
         if (mTouchCameFromNavBar) {
@@ -153,7 +99,7 @@
 
     @Override
     public void onSwipeUpCancelled() {
-        if (mGestureCallback != null && !mAssistantGestureActive) {
+        if (mGestureCallback != null) {
             mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
         }
     }
@@ -166,52 +112,16 @@
             case MotionEvent.ACTION_DOWN:
                 mDownPos.set(event.getX(), event.getY());
                 mLastPos.set(mDownPos);
-                mTouchCameFromAssistantCorner =
-                        mAssistantLeftRegion.contains(event.getX(), event.getY())
-                                || mAssistantRightRegion.contains(event.getX(), event.getY());
-                mAssistantGestureActive = mTouchCameFromAssistantCorner;
-                mTouchCameFromNavBar = !mTouchCameFromAssistantCorner
-                        && mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
+                mTouchCameFromNavBar = mDownPos.y >= mDisplaySize.y - mBottomGestureHeight;
                 if (!mTouchCameFromNavBar && mGestureCallback != null) {
                     mGestureCallback.setNavBarGestureProgress(null);
                 }
-                mLaunchedAssistant = false;
                 mSwipeUpTouchTracker.init();
                 mMotionPauseDetector.clear();
                 mMotionPauseDetector.setOnMotionPauseListener(this);
                 break;
             case MotionEvent.ACTION_MOVE:
                 mLastPos.set(event.getX(), event.getY());
-                if (!mAssistantGestureActive) {
-                    break;
-                }
-
-                if (!mPassedAssistantSlop) {
-                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
-                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
-                            > mAssistantSquaredSlop) {
-
-                        mPassedAssistantSlop = true;
-                        mAssistantStartDragPos.set(mLastPos.x, mLastPos.y);
-                        mAssistantDragStartTime = SystemClock.uptimeMillis();
-
-                        mAssistantGestureActive = isValidAssistantGestureAngle(
-                                mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y);
-                        if (!mAssistantGestureActive && mGestureCallback != null) {
-                            mGestureCallback.onNavBarGestureAttempted(
-                                    ASSISTANT_NOT_STARTED_BAD_ANGLE, new PointF());
-                        }
-                    }
-                } else {
-                    // Movement
-                    mAssistantDistance = (float) Math.hypot(mLastPos.x - mAssistantStartDragPos.x,
-                            mLastPos.y - mAssistantStartDragPos.y);
-                    if (mAssistantDistance >= 0) {
-                        final long diff = SystemClock.uptimeMillis() - mAssistantDragStartTime;
-                        mAssistantTimeFraction = Math.min(diff * 1f / mAssistantTimeThreshold, 1);
-                        updateAssistantProgress();
-                    }
-                }
                 break;
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
@@ -222,35 +132,19 @@
                     intercepted = true;
                     break;
                 }
-                if (mAssistantGestureActive && !mLaunchedAssistant && mGestureCallback != null) {
-                    mGestureCallback.onNavBarGestureAttempted(
-                            ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT, new PointF());
-                    ValueAnimator animator = ValueAnimator.ofFloat(mAssistantLastProgress, 0)
-                            .setDuration(RETRACT_GESTURE_ANIMATION_DURATION_MS);
-                    animator.addUpdateListener(valueAnimator -> {
-                        float progress = (float) valueAnimator.getAnimatedValue();
-                        mGestureCallback.setAssistantProgress(progress);
-                    });
-                    animator.setInterpolator(Interpolators.DEACCEL_2);
-                    animator.start();
-                }
-                mPassedAssistantSlop = false;
                 break;
         }
         if (mTouchCameFromNavBar && mGestureCallback != null) {
             mGestureCallback.setNavBarGestureProgress(event.getY() - mDownPos.y);
         }
         mSwipeUpTouchTracker.onMotionEvent(event);
-        mAssistantGestureDetector.onTouchEvent(event);
         mMotionPauseDetector.addPosition(event);
         mMotionPauseDetector.setDisallowPause(mLastPos.y >= mDisplaySize.y - mBottomGestureHeight);
         return intercepted;
     }
 
     boolean onInterceptTouch(MotionEvent event) {
-        return mAssistantLeftRegion.contains(event.getX(), event.getY())
-                || mAssistantRightRegion.contains(event.getX(), event.getY())
-                || event.getY() >= mDisplaySize.y - mBottomGestureHeight;
+        return event.getY() >= mDisplaySize.y - mBottomGestureHeight;
     }
 
     @Override
@@ -263,39 +157,6 @@
         VibratorWrapper.INSTANCE.get(mContext).vibrate(OVERVIEW_HAPTIC);
     }
 
-    /**
-     * Determine if angle is larger than threshold for assistant detection
-     */
-    private boolean isValidAssistantGestureAngle(float deltaX, float deltaY) {
-        float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
-
-        // normalize so that angle is measured clockwise from horizontal in the bottom right corner
-        // and counterclockwise from horizontal in the bottom left corner
-        angle = angle > 90 ? 180 - angle : angle;
-        return (angle > mAssistantAngleThreshold && angle < 90);
-    }
-
-    private void updateAssistantProgress() {
-        if (!mLaunchedAssistant) {
-            mAssistantLastProgress =
-                    Math.min(mAssistantDistance * 1f / mAssistantDragDistThreshold, 1)
-                            * mAssistantTimeFraction;
-            if (mAssistantDistance >= mAssistantDragDistThreshold && mAssistantTimeFraction >= 1) {
-                startAssistant(new PointF());
-            } else if (mGestureCallback != null) {
-                mGestureCallback.setAssistantProgress(mAssistantLastProgress);
-            }
-        }
-    }
-
-    private void startAssistant(PointF velocity) {
-        if (mGestureCallback != null) {
-            mGestureCallback.onNavBarGestureAttempted(ASSISTANT_COMPLETED, velocity);
-        }
-        VibratorWrapper.INSTANCE.get(mContext).vibrate(VibratorWrapper.EFFECT_CLICK);
-        mLaunchedAssistant = true;
-    }
-
     enum NavBarGestureResult {
         UNKNOWN,
         HOME_GESTURE_COMPLETED,
@@ -304,9 +165,6 @@
         OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
         HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION,  // Side swipe on nav bar.
         HOME_OR_OVERVIEW_CANCELLED,
-        ASSISTANT_COMPLETED,
-        ASSISTANT_NOT_STARTED_BAD_ANGLE,
-        ASSISTANT_NOT_STARTED_SWIPE_TOO_SHORT,
     }
 
     /** Callback to let the UI react to attempted nav bar gestures. */
@@ -319,27 +177,5 @@
 
         /** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
         default void setNavBarGestureProgress(@Nullable Float displacement) {}
-
-        /** Indicates the progress of an Assistant gesture. */
-        default void setAssistantProgress(float progress) {}
-    }
-
-    private class AssistantGestureListener extends GestureDetector.SimpleOnGestureListener {
-        @Override
-        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
-            if (!mLaunchedAssistant && mTouchCameFromAssistantCorner) {
-                PointF velocity = new PointF(velocityX, velocityY);
-                if (!isValidAssistantGestureAngle(velocityX, -velocityY)) {
-                    if (mGestureCallback != null) {
-                        mGestureCallback.onNavBarGestureAttempted(ASSISTANT_NOT_STARTED_BAD_ANGLE,
-                                velocity);
-                    }
-                } else if (mAssistantDistance >= mAssistantFlingDistThreshold) {
-                    mAssistantLastProgress = 1;
-                    startAssistant(velocity);
-                }
-            }
-            return true;
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
index 160e431..dfbcf4d 100644
--- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java
@@ -111,6 +111,7 @@
                     case BACK_COMPLETED_FROM_RIGHT:
                     case BACK_CANCELLED_FROM_LEFT:
                     case BACK_CANCELLED_FROM_RIGHT:
+                    case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
                         showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
                         break;
                 }
diff --git a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
index 19b7933..f0bd4f9 100644
--- a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
@@ -49,10 +49,6 @@
     @Override
     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
         switch (result) {
-            case ASSISTANT_COMPLETED:
-                showRippleEffect(null);
-                showFeedback(R.string.sandbox_mode_assistant_gesture_feedback_successful);
-                break;
             case HOME_GESTURE_COMPLETED:
                 animateFakeTaskViewHome(finalVelocity, () -> {
                     showFeedback(R.string.sandbox_mode_home_gesture_feedback_successful);
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 3faa7e4..25de605 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -117,9 +117,6 @@
             case OVERVIEW_NAVIGATION:
             case OVERVIEW_NAVIGATION_COMPLETE:
                 return new OverviewGestureTutorialFragment(fromTutorialMenu);
-            case ASSISTANT:
-            case ASSISTANT_COMPLETE:
-                return new AssistantGestureTutorialFragment(fromTutorialMenu);
             case SANDBOX_MODE:
                 return new SandboxModeTutorialFragment(fromTutorialMenu);
             default:
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
index 2ae512a..e8cadab 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java
@@ -23,7 +23,6 @@
 
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.ui.TaplTestsLauncher3;
-import com.android.launcher3.util.rule.TestStabilityRule;
 import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch;
 
 import org.junit.After;
@@ -61,10 +60,6 @@
     }
 
     @Test
-    // TODO (b/270201357): When this test is proven stable, remove this TestStabilityRule and
-    //  introduce into presubmit as well.
-    @TestStabilityRule.Stability(
-            flavors = TestStabilityRule.LOCAL | TestStabilityRule.PLATFORM_POSTSUBMIT)
     @PortraitLandscape
     @TaskbarModeSwitch
     public void testSplitAppFromHomeWithItself() throws Exception {
diff --git a/res/drawable/taskbar_divider_bg.xml b/res/color-night-v31/taskbar_divider_background.xml
similarity index 69%
copy from res/drawable/taskbar_divider_bg.xml
copy to res/color-night-v31/taskbar_divider_background.xml
index a8c2ae7..1981eec 100644
--- a/res/drawable/taskbar_divider_bg.xml
+++ b/res/color-night-v31/taskbar_divider_background.xml
@@ -13,9 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<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="1dp" />
-</shape>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:color="#797869" android:lStar="60" />
+</selector>
diff --git a/res/drawable/taskbar_divider_bg.xml b/res/color-v31/taskbar_divider_background.xml
similarity index 69%
copy from res/drawable/taskbar_divider_bg.xml
copy to res/color-v31/taskbar_divider_background.xml
index a8c2ae7..487e791 100644
--- a/res/drawable/taskbar_divider_bg.xml
+++ b/res/color-v31/taskbar_divider_background.xml
@@ -13,9 +13,6 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<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="1dp" />
-</shape>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:color="#797869" android:lStar="50" />
+</selector>
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index f041ffb..5fbd48c 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -16,8 +16,8 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.pm.UserCache;
@@ -52,7 +52,7 @@
      * Updates the app widgets whose id has changed during the restore process.
      */
     @WorkerThread
-    public static void restoreAppWidgetIds(Context context, DatabaseHelper helper,
+    public static void restoreAppWidgetIds(Context context, ModelDbController controller,
             int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host) {
         if (WidgetsModel.GO_DISABLE_WIDGETS) {
             Log.e(TAG, "Skipping widget ID remap as widgets not supported");
@@ -92,12 +92,12 @@
             final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?";
             final String[] args = new String[] { oldWidgetId, Long.toString(mainProfileId) };
             int result = new ContentWriter(context,
-                            new ContentWriter.CommitParams(helper, where, args))
+                            new ContentWriter.CommitParams(controller, where, args))
                     .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
                     .put(LauncherSettings.Favorites.RESTORED, state)
                     .commit();
             if (result == 0) {
-                Cursor cursor = helper.getWritableDatabase().query(
+                Cursor cursor = controller.getDb().query(
                         Favorites.TABLE_NAME,
                         new String[] {Favorites.APPWIDGET_ID},
                         "appWidgetId=?", new String[] { oldWidgetId }, null, null, null);
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 0988769..5163ede 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -85,6 +85,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.List;
 import java.util.Stack;
 
 public class CellLayout extends ViewGroup {
@@ -891,7 +892,7 @@
      *
      * @param result Array of 2 ints to hold the x and y coordinate of the point
      */
-    void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
+    public void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) {
         cellToRect(cellX, cellY, spanX, spanY, mTempRect);
         result[0] = mTempRect.centerX();
         result[1] = mTempRect.centerY();
@@ -2340,7 +2341,16 @@
         }
         Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY);
         Rect r1 = new Rect();
-        for (View child: solution.map.keySet()) {
+        // The views need to be sorted so that the results are deterministic on the views positions
+        // and not by the views hash which is "random".
+        // The views are sorted twice, once for the X position and a second time for the Y position
+        // to ensure same order everytime.
+        Comparator comparator = Comparator.comparing(view ->
+                        ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellX())
+                .thenComparing(view ->
+                        ((CellLayoutLayoutParams) ((View) view).getLayoutParams()).getCellY());
+        List<View> views = solution.map.keySet().stream().sorted(comparator).toList();
+        for (View child : views) {
             if (child == ignoreView) continue;
             CellAndSpan c = solution.map.get(child);
             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4dfd918..86a5f9f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -56,6 +56,14 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC;
+import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC;
+import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.COLD;
+import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.COLD_DEVICE_REBOOTING;
+import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.WARM;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
 import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
 import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
@@ -64,7 +72,6 @@
 import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.ItemInfoMatcher.forFolderMatch;
 
 import android.animation.Animator;
@@ -111,6 +118,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.OnPreDrawListener;
 import android.view.WindowManager.LayoutParams;
 import android.view.accessibility.AccessibilityEvent;
@@ -157,6 +165,7 @@
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.InstanceIdSequence;
+import com.android.launcher3.logging.StartupLatencyLogger;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ItemInstallQueue;
@@ -190,6 +199,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
@@ -293,6 +303,8 @@
     public static final String ON_RESUME_EVT = "Launcher.onResume";
     public static final String ON_NEW_INTENT_EVT = "Launcher.onNewIntent";
 
+    private static boolean sIsNewProcess = true;
+
     private StateManager<LauncherState> mStateManager;
 
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
@@ -404,12 +416,22 @@
 
     private StringCache mStringCache;
     private BaseSearchConfig mBaseSearchConfig;
-
+    private StartupLatencyLogger mStartupLatencyLogger;
     private CellPosMapper mCellPosMapper = CellPosMapper.DEFAULT;
 
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
+        mStartupLatencyLogger = createStartupLatencyLogger(
+                sIsNewProcess
+                        ? LockedUserState.get(this).isUserUnlockedAtLauncherStartup()
+                            ? COLD
+                            : COLD_DEVICE_REBOOTING
+                        : WARM);
+        sIsNewProcess = false;
+        mStartupLatencyLogger
+                .logStart(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
+                .logStart(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
         // Only use a hard-coded cookie since we only want to trace this once.
         if (Utilities.ATLEAST_S) {
             Trace.beginAsyncSection(
@@ -518,6 +540,7 @@
             }
         }
 
+        mStartupLatencyLogger.logWorkspaceLoadStartTime();
         if (!mModel.addCallbacksAndLoad(this)) {
             if (!internalStateHandled) {
                 // If we are not binding synchronously, pause drawing until initial bind complete,
@@ -557,6 +580,17 @@
             getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
         }
         setTitle(R.string.home_screen);
+        mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
+    }
+
+    /**
+     * Create {@link StartupLatencyLogger} that only collects launcher startup latency metrics
+     * without sending them anywhere. Child class can override this method to create logger
+     * that overrides {@link StartupLatencyLogger#log()} to report those metrics.
+     */
+    protected StartupLatencyLogger createStartupLatencyLogger(
+            StatsLogManager.StatsLatencyLogger.LatencyType latencyType) {
+        return new StartupLatencyLogger(latencyType);
     }
 
     /**
@@ -1290,7 +1324,10 @@
      * Finds all the views we need and configure them properly.
      */
     protected void setupViews() {
+        mStartupLatencyLogger.logStart(LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION);
         inflateRootView(R.layout.launcher);
+        mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION);
+
         mDragLayer = findViewById(R.id.drag_layer);
         mFocusHandler = mDragLayer.getFocusIndicatorHelper();
         mWorkspace = mDragLayer.findViewById(R.id.workspace);
@@ -2695,7 +2732,8 @@
 
     @Override
     @TargetApi(Build.VERSION_CODES.S)
-    public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+    public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+            int workspaceItemCount, boolean isBindSync) {
         mSynchronouslyBoundPages = boundPages;
         mPagesToBindSynchronously = new IntSet();
 
@@ -2719,6 +2757,26 @@
             Trace.endAsyncSection(DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
                     DISPLAY_WORKSPACE_TRACE_COOKIE);
         }
+        mStartupLatencyLogger
+                .logCardinality(workspaceItemCount)
+                .logEnd(isBindSync
+                        ? LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        : LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC);
+        // In the first rootview's onDraw after onInitialBindComplete(), log end of startup latency.
+        getRootView().getViewTreeObserver().addOnDrawListener(
+                new ViewTreeObserver.OnDrawListener() {
+
+                    @Override
+                    public void onDraw() {
+                        mStartupLatencyLogger
+                                .logEnd(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
+                                .log()
+                                .reset();
+                        MAIN_EXECUTOR.getHandler().postAtFrontOfQueue(
+                                () -> getRootView().getViewTreeObserver()
+                                        .removeOnDrawListener(this));
+                    }
+                });
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 6798667d..d3e94e1 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -417,7 +417,7 @@
                 if (bindDirectly) {
                     // Divide the set of loaded items into those that we are binding synchronously,
                     // and everything else that is to be bound normally (asynchronously).
-                    launcherBinder.bindWorkspace(bindAllCallbacks);
+                    launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true);
                     // For now, continue posting the binding of AllApps as there are other
                     // issues that arise from that.
                     launcherBinder.bindAllApps();
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 0df4bd4..9abec50 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -263,18 +263,6 @@
                 getModelDbController().refreshHotseatRestoreTable();
                 return null;
             }
-            case LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER: {
-                Bundle result = new Bundle();
-                result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().updateCurrentOpenHelper(arg /* dbFile */));
-                return result;
-            }
-            case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
-                Bundle result = new Bundle();
-                result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
-                        getModelDbController().prepareForPreview(arg /* dbFile */));
-                return result;
-            }
         }
         return null;
     }
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index b65e96b..7fda326 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -148,11 +148,6 @@
         public static final String HYBRID_HOTSEAT_BACKUP_TABLE = "hotseat_restore_backup";
 
         /**
-         * Temporary table used specifically for grid migrations during wallpaper preview
-         */
-        public static final String PREVIEW_TABLE_NAME = "favorites_preview";
-
-        /**
          * Temporary table used specifically for multi-db grid migrations
          */
         public static final String TMP_TABLE = "favorites_tmp";
@@ -164,18 +159,6 @@
                 + LauncherProvider.AUTHORITY + "/" + TABLE_NAME);
 
         /**
-         * The content:// style URL for "favorites_preview" table
-         */
-        public static final Uri PREVIEW_CONTENT_URI = Uri.parse("content://"
-                + LauncherProvider.AUTHORITY + "/" + PREVIEW_TABLE_NAME);
-
-        /**
-         * The content:// style URL for "favorites_tmp" table
-         */
-        public static final Uri TMP_CONTENT_URI = Uri.parse("content://"
-                + LauncherProvider.AUTHORITY + "/" + TMP_TABLE);
-
-        /**
          * The content:// style URL for a given row, identified by its id.
          *
          * @param id The row id.
@@ -376,10 +359,6 @@
 
         public static final String METHOD_REFRESH_HOTSEAT_RESTORE_TABLE = "restore_hotseat_table";
 
-        public static final String METHOD_UPDATE_CURRENT_OPEN_HELPER = "update_current_open_helper";
-
-        public static final String METHOD_PREP_FOR_PREVIEW = "prep_for_preview";
-
         public static final String EXTRA_VALUE = "value";
 
         public static final String EXTRA_DB_NAME = "db_name";
@@ -393,11 +372,8 @@
         }
 
         public static Bundle call(ContentResolver cr, String method, String arg) {
-            return call(cr, method, arg, null /* extras */);
+            return cr.call(CONTENT_URI, method, arg, null);
         }
 
-        public static Bundle call(ContentResolver cr, String method, String arg, Bundle extras) {
-            return cr.call(CONTENT_URI, method, arg, extras);
-        }
     }
 }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 372e9bf..8f0b8ec 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.graphics;
 
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
@@ -52,6 +53,8 @@
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.GridSizeMigrationUtil;
 import com.android.launcher3.model.LoaderTask;
+import com.android.launcher3.model.ModelDbController;
+import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Themes;
@@ -145,7 +148,9 @@
         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 
-        try (Cursor c = context.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+        ModelDbController mainController =
+                LauncherAppState.getInstance(mContext).getModel().getModelDbController();
+        try (Cursor c = mainController.query(TABLE_NAME,
                 new String[] {
                         LauncherSettings.Favorites.APPWIDGET_ID,
                         LauncherSettings.Favorites.SPANX,
@@ -190,8 +195,6 @@
 
     @WorkerThread
     private void loadModelData() {
-        final boolean migrated = doGridMigrationIfNecessary();
-
         final Context inflationContext;
         if (mWallpaperColors != null) {
             // Create a themed context, without affecting the main application context
@@ -209,8 +212,20 @@
                     Themes.getActivityThemeRes(mContext));
         }
 
-        if (migrated) {
+        if (GridSizeMigrationUtil.needsToMigrate(inflationContext, mIdp)) {
+            // Start the migration
             PreviewContext previewContext = new PreviewContext(inflationContext, mIdp);
+            // Copy existing data to preview DB
+            LauncherDbUtils.copyTable(LauncherAppState.getInstance(mContext)
+                    .getModel().getModelDbController().getDb(),
+                    TABLE_NAME,
+                    LauncherAppState.getInstance(previewContext)
+                            .getModel().getModelDbController().getDb(),
+                    TABLE_NAME,
+                    mContext);
+            LauncherAppState.getInstance(previewContext)
+                    .getModel().getModelDbController().clearEmptyDbFlag();
+
             new LoaderTask(
                     LauncherAppState.getInstance(previewContext),
                     /* bgAllAppsList= */ null,
@@ -229,8 +244,7 @@
                         query += " or " + LauncherSettings.Favorites.SCREEN + " = "
                                 + Workspace.SECOND_SCREEN_ID;
                     }
-                    loadWorkspaceForPreviewSurfaceRenderer(new ArrayList<>(),
-                            LauncherSettings.Favorites.PREVIEW_CONTENT_URI, query);
+                    loadWorkspace(new ArrayList<>(), query, null);
 
                     final SparseArray<Size> spanInfo =
                             getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
@@ -253,14 +267,6 @@
         }
     }
 
-    @WorkerThread
-    private boolean doGridMigrationIfNecessary() {
-        if (!GridSizeMigrationUtil.needsToMigrate(mContext, mIdp)) {
-            return false;
-        }
-        return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, mIdp);
-    }
-
     @UiThread
     private void renderView(Context inflationContext, BgDataModel dataModel,
             Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap,
diff --git a/src/com/android/launcher3/logging/StartupLatencyLogger.kt b/src/com/android/launcher3/logging/StartupLatencyLogger.kt
new file mode 100644
index 0000000..435f798
--- /dev/null
+++ b/src/com/android/launcher3/logging/StartupLatencyLogger.kt
@@ -0,0 +1,203 @@
+package com.android.launcher3.logging
+
+import android.os.SystemClock
+import android.util.Log
+import android.util.SparseLongArray
+import androidx.annotation.MainThread
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.contains
+import androidx.core.util.isEmpty
+import com.android.launcher3.BuildConfig
+import com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent
+import com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType
+import com.android.launcher3.util.Preconditions
+
+/** Logger for logging Launcher activity's startup latency. */
+open class StartupLatencyLogger(val latencyType: LatencyType) {
+
+    companion object {
+        const val TAG = "LauncherStartupLatencyLogger"
+        const val UNSET_INT = -1
+        const val UNSET_LONG = -1L
+    }
+
+    @VisibleForTesting val startTimeByEvent = SparseLongArray()
+    @VisibleForTesting val endTimeByEvent = SparseLongArray()
+
+    @VisibleForTesting var cardinality: Int = UNSET_INT
+    @VisibleForTesting var workspaceLoadStartTime: Long = UNSET_LONG
+
+    private var isInTest = false
+
+    /** Subclass can override this method to handle collected latency metrics. */
+    @MainThread
+    open fun log(): StartupLatencyLogger {
+        return this
+    }
+
+    @MainThread
+    fun logWorkspaceLoadStartTime() = logWorkspaceLoadStartTime(SystemClock.elapsedRealtime())
+
+    @VisibleForTesting
+    @MainThread
+    fun logWorkspaceLoadStartTime(startTimeMs: Long): StartupLatencyLogger {
+        Preconditions.assertUIThread()
+        workspaceLoadStartTime = startTimeMs
+        return this
+    }
+
+    /**
+     * Log size of workspace. Larger number of workspace items (icons, folders, widgets) means
+     * longer latency to initialize workspace.
+     */
+    @MainThread
+    fun logCardinality(cardinality: Int): StartupLatencyLogger {
+        Preconditions.assertUIThread()
+        this.cardinality = cardinality
+        return this
+    }
+
+    @MainThread
+    fun logStart(event: LauncherLatencyEvent) = logStart(event, SystemClock.elapsedRealtime())
+
+    @MainThread
+    fun logStart(event: LauncherLatencyEvent, startTimeMs: Long): StartupLatencyLogger {
+        // In unit test no looper is attached to current thread
+        Preconditions.assertUIThread()
+        if (validateLoggingEventAtStart(event)) {
+            startTimeByEvent.put(event.id, startTimeMs)
+        }
+        return this
+    }
+
+    @MainThread
+    fun logEnd(event: LauncherLatencyEvent) = logEnd(event, SystemClock.elapsedRealtime())
+
+    @MainThread
+    fun logEnd(event: LauncherLatencyEvent, endTimeMs: Long): StartupLatencyLogger {
+        // In unit test no looper is attached to current thread
+        Preconditions.assertUIThread()
+        maybeLogStartOfWorkspaceLoadTime(event)
+        if (validateLoggingEventAtEnd(event)) {
+            endTimeByEvent.put(event.id, endTimeMs)
+        }
+
+        return this
+    }
+
+    @MainThread
+    fun reset() {
+        // In unit test no looper is attached to current thread
+        Preconditions.assertUIThread()
+        startTimeByEvent.clear()
+        endTimeByEvent.clear()
+        cardinality = UNSET_INT
+        workspaceLoadStartTime = UNSET_LONG
+    }
+
+    @MainThread
+    private fun maybeLogStartOfWorkspaceLoadTime(event: LauncherLatencyEvent) {
+        if (workspaceLoadStartTime == UNSET_LONG) {
+            return
+        }
+        if (
+            event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC ||
+                event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC
+        ) {
+            logStart(event, workspaceLoadStartTime)
+            workspaceLoadStartTime = UNSET_LONG
+        }
+    }
+
+    /** @return true if we can log start of [LauncherLatencyEvent] and vice versa. */
+    @MainThread
+    private fun validateLoggingEventAtStart(event: LauncherLatencyEvent): Boolean {
+        if (!BuildConfig.IS_STUDIO_BUILD && !isInTest) {
+            return true
+        }
+        if (startTimeByEvent.contains(event.id)) {
+            Log.e(TAG, String.format("Cannot restart same %s event", event.name))
+            return false
+        } else if (
+            startTimeByEvent.isEmpty() &&
+                event != LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION
+        ) {
+            Log.e(
+                TAG,
+                String.format(
+                    "The first log start event must be %s.",
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.name
+                )
+            )
+            return false
+        } else if (
+            event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC &&
+                startTimeByEvent.get(
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC.id
+                ) != 0L
+        ) {
+            Log.e(
+                TAG,
+                String.format(
+                    "Cannot start %s event after %s starts",
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC.name,
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC.name
+                )
+            )
+            return false
+        } else if (
+            event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC &&
+                startTimeByEvent.get(
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC.id
+                ) != 0L
+        ) {
+            Log.e(
+                TAG,
+                String.format(
+                    "Cannot start %s event after %s starts",
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC.name,
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC.name
+                )
+            )
+            return false
+        }
+
+        return true
+    }
+
+    /** @return true if we can log end of [LauncherLatencyEvent] and vice versa. */
+    @MainThread
+    private fun validateLoggingEventAtEnd(event: LauncherLatencyEvent): Boolean {
+        if (!BuildConfig.IS_STUDIO_BUILD && !isInTest) {
+            return true
+        }
+        if (!startTimeByEvent.contains(event.id)) {
+            Log.e(TAG, String.format("Cannot end %s event before starting it", event.name))
+            return false
+        } else if (endTimeByEvent.contains(event.id)) {
+            Log.e(TAG, String.format("Cannot end same %s event again", event.name))
+            return false
+        } else if (
+            event != LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION &&
+                endTimeByEvent.contains(
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.id
+                )
+        ) {
+            Log.e(
+                TAG,
+                String.format(
+                    "Cannot end %s event after %s",
+                    event.name,
+                    LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.name
+                )
+            )
+            return false
+        }
+        return true
+    }
+
+    @VisibleForTesting
+    fun setIsInTest() {
+        isInTest = true
+    }
+}
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 5b4a02b..8197b73 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -657,6 +657,39 @@
         }
     }
 
+    /** Launcher's latency events. */
+    public enum LauncherLatencyEvent implements EventEnum {
+        // Details of below 6 events with prefix of "LAUNCHER_LATENCY_STARTUP_" are discussed in
+        // go/launcher-startup-latency
+        @UiEvent(doc = "The total duration of launcher startup latency.")
+        LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION(1362),
+
+        @UiEvent(doc = "The duration of launcher activity's onCreate().")
+        LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE(1363),
+
+        @UiEvent(doc =
+                "The duration to inflate launcher root view in launcher activity's onCreate().")
+        LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION(1364),
+
+        @UiEvent(doc = "The duration of synchronous loading workspace")
+        LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC(1366),
+
+        @UiEvent(doc = "The duration of asynchronous loading workspace")
+        LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC(1367),
+        ;
+
+        private final int mId;
+
+        LauncherLatencyEvent(int id) {
+            mId = id;
+        }
+
+        @Override
+        public int getId() {
+            return mId;
+        }
+    }
+
     /**
      * Launcher specific ranking related events.
      */
@@ -807,7 +840,10 @@
             CONTROLLED(7),
             CACHED(8),
             // example: device is rebooting via power key or shell command `adb reboot`
-            COLD_DEVICE_REBOOTING(9);
+            COLD_DEVICE_REBOOTING(9),
+            // Tracking warm startup latency:
+            // https://developer.android.com/topic/performance/vitals/launch-time#warm
+            WARM(10);
             private final int mId;
 
             LatencyType(int id) {
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index c946e2c..ba9eb20 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -78,14 +78,14 @@
     /**
      * Binds all loaded data to actual views on the main thread.
      */
-    public void bindWorkspace(boolean incrementBindId) {
+    public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
         if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
             DisjointWorkspaceBinder workspaceBinder =
                     initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
-            workspaceBinder.bindCurrentWorkspacePages();
+            workspaceBinder.bindCurrentWorkspacePages(isBindSync);
             workspaceBinder.bindOtherWorkspacePages();
         } else {
-            bindWorkspaceAllAtOnce(incrementBindId);
+            bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
         }
     }
 
@@ -108,13 +108,13 @@
         }
     }
 
-    private void bindWorkspaceAllAtOnce(boolean incrementBindId) {
+    private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) {
         // Save a copy of all the bg-thread collections
         ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
         ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
         final IntArray orderedScreenIds = new IntArray();
         ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
-
+        final int workspaceItemCount;
         synchronized (mBgDataModel) {
             workspaceItems.addAll(mBgDataModel.workspaceItems);
             appWidgets.addAll(mBgDataModel.appWidgets);
@@ -124,11 +124,13 @@
                 mBgDataModel.lastBindId++;
             }
             mMyBindingId = mBgDataModel.lastBindId;
+            workspaceItemCount = mBgDataModel.itemsIdMap.size();
         }
 
         for (Callbacks cb : mCallbacksList) {
             new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
-                    workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
+                    workspaceItems, appWidgets, extraItems, orderedScreenIds)
+                    .bind(isBindSync, workspaceItemCount);
         }
     }
 
@@ -246,7 +248,7 @@
             mOrderedScreenIds = orderedScreenIds;
         }
 
-        private void bind() {
+        private void bind(boolean isBindSync, int workspaceItemCount) {
             final IntSet currentScreenIds =
                     mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
             Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);
@@ -297,7 +299,8 @@
             executeCallbacksTask(
                     c -> {
                         MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                        c.onInitialBindComplete(currentScreenIds, pendingTasks);
+                        c.onInitialBindComplete(
+                                currentScreenIds, pendingTasks, workspaceItemCount, isBindSync);
                     }, mUiExecutor);
 
             mCallbacks.bindStringCache(mBgDataModel.stringCache.clone());
@@ -361,18 +364,19 @@
          * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
          * not bind any items.
          */
-        protected void bindCurrentWorkspacePages() {
+        protected void bindCurrentWorkspacePages(boolean isBindSync) {
             // Save a copy of all the bg-thread collections
             ArrayList<ItemInfo> workspaceItems;
             ArrayList<LauncherAppWidgetInfo> appWidgets;
             ArrayList<FixedContainerItems> fciList = new ArrayList<>();
-
+            final int workspaceItemCount;
             synchronized (mBgDataModel) {
                 workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
                 appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
                 if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                     mBgDataModel.extraItems.forEach(fciList::add);
                 }
+                workspaceItemCount = mBgDataModel.itemsIdMap.size();
             }
 
             workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
@@ -395,10 +399,10 @@
 
             bindWorkspaceItems(workspaceItems);
             bindAppWidgets(appWidgets);
-
             executeCallbacksTask(c -> {
                 MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-                c.onInitialBindComplete(mCurrentScreenIds, new RunnableList());
+                c.onInitialBindComplete(
+                        mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync);
             }, mUiExecutor);
         }
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 0e3b06c..0861e9d 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -484,7 +484,9 @@
         default void bindWorkspaceComponentsRemoved(Predicate<ItemInfo> matcher) { }
         default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
 
-        default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+        /** Called when workspace has been bound. */
+        default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+                int workspaceItemCount, boolean isBindSync) {
             pendingTasks.executeAllAndDestroy();
         }
 
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index dc5fcf7..ecf5f67 100644
--- a/src/com/android/launcher3/model/DatabaseHelper.java
+++ b/src/com/android/launcher3/model/DatabaseHelper.java
@@ -15,8 +15,8 @@
  */
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
-import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -36,9 +36,6 @@
 
 import com.android.launcher3.AutoInstallsLayout;
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherFiles;
-import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
@@ -58,6 +55,7 @@
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.function.ToLongFunction;
 import java.util.stream.Collectors;
 
 /**
@@ -76,45 +74,23 @@
     private static final boolean LOGD = false;
 
     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
-    public static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
     private final Context mContext;
-    private final boolean mForMigration;
+    private final ToLongFunction<UserHandle> mUserSerialProvider;
+    private final Runnable mOnEmptyDbCreateCallback;
+
     private int mMaxItemId = -1;
     public boolean mHotseatRestoreTableExists;
 
-    public static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
-        return createDatabaseHelper(context, null, forMigration);
-    }
-
-    public static DatabaseHelper createDatabaseHelper(Context context, String dbName,
-            boolean forMigration) {
-        if (dbName == null) {
-            dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
-        }
-        DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
-        // Table creation sometimes fails silently, which leads to a crash loop.
-        // This way, we will try to create a table every time after crash, so the device
-        // would eventually be able to recover.
-        if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
-            Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
-            // This operation is a no-op if the table already exists.
-            databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
-        }
-        databaseHelper.mHotseatRestoreTableExists = tableExists(
-                databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
-
-        databaseHelper.initIds();
-        return databaseHelper;
-    }
-
     /**
      * Constructor used in tests and for restore.
      */
-    public DatabaseHelper(Context context, String dbName, boolean forMigration) {
+    public DatabaseHelper(Context context, String dbName,
+            ToLongFunction<UserHandle> userSerialProvider, Runnable onEmptyDbCreateCallback) {
         super(context, dbName, SCHEMA_VERSION);
         mContext = context;
-        mForMigration = forMigration;
+        mUserSerialProvider = userSerialProvider;
+        mOnEmptyDbCreateCallback = onEmptyDbCreateCallback;
     }
 
     protected void initIds() {
@@ -131,13 +107,11 @@
 
         mMaxItemId = 1;
 
-        addFavoritesTable(db, false);
+        addTableToDb(db, getDefaultUserSerial(), false /* optional */);
 
         // Fresh and clean launcher DB.
         mMaxItemId = initializeMaxItemId(db);
-        if (!mForMigration) {
-            onEmptyDbCreated();
-        }
+        mOnEmptyDbCreateCallback.run();
     }
 
     public void onAddOrDeleteOp(SQLiteDatabase db) {
@@ -147,38 +121,8 @@
         }
     }
 
-    /**
-     * Re-composite given key in respect to database. If the current db is
-     * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
-     * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
-     * string will be "EMPTY_DATABASE_CREATED@minimal.db".
-     */
-    public String getKey(final String key) {
-        if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
-            return key;
-        }
-        return key + "@" + getDatabaseName();
-    }
-
-    /**
-     * Overridden in tests.
-     */
-    protected void onEmptyDbCreated() {
-        // Set the flag for empty DB
-        LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
-                .commit();
-    }
-
-    public long getSerialNumberForUser(UserHandle user) {
-        return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
-    }
-
-    public long getDefaultUserSerial() {
-        return getSerialNumberForUser(Process.myUserHandle());
-    }
-
-    private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
-        Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
+    private long getDefaultUserSerial() {
+        return mUserSerialProvider.applyAsLong(Process.myUserHandle());
     }
 
     @Override
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index eded5ea..9a6cde6 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 
 import android.content.ComponentName;
@@ -34,16 +37,15 @@
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.graphics.LauncherPreviewRenderer;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.widget.WidgetManagerHelper;
 
@@ -89,81 +91,38 @@
         return needsToMigrate;
     }
 
-    /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
-    public static boolean migrateGridIfNeeded(Context context) {
-        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
-            return true;
-        }
-        return migrateGridIfNeeded(context, null);
-    }
-
     /**
-     * When migrating the grid for preview, we copy the table
-     * {@link LauncherSettings.Favorites#TABLE_NAME} into
-     * {@link LauncherSettings.Favorites#PREVIEW_TABLE_NAME}, run grid size migration from the
-     * former to the later, then use the later table for preview.
-     *
-     * Similarly when doing the actual grid migration, the former grid option's table
-     * {@link LauncherSettings.Favorites#TABLE_NAME} is copied into the new grid option's
-     * {@link LauncherSettings.Favorites#TMP_TABLE}, we then run the grid size migration algorithm
+     * When migrating the grid, we copy the table
+     * {@link LauncherSettings.Favorites#TABLE_NAME} from {@code source} into
+     * {@link LauncherSettings.Favorites#TMP_TABLE}, run the grid size migration algorithm
      * to migrate the later to the former, and load the workspace from the default
      * {@link LauncherSettings.Favorites#TABLE_NAME}.
      *
      * @return false if the migration failed.
      */
-    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
-        boolean migrateForPreview = idp != null;
-        if (!migrateForPreview) {
-            idp = LauncherAppState.getIDP(context);
-        }
+    public static boolean migrateGridIfNeeded(
+            @NonNull Context context,
+            @NonNull InvariantDeviceProfile idp,
+            @NonNull DatabaseHelper target,
+            @NonNull SQLiteDatabase source) {
 
         DeviceGridState srcDeviceState = new DeviceGridState(context);
         DeviceGridState destDeviceState = new DeviceGridState(idp);
         if (!needsToMigrate(srcDeviceState, destDeviceState)) {
             return true;
         }
+        copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context);
 
         HashSet<String> validPackages = getValidPackages(context);
-
-        if (migrateForPreview) {
-            if (!LauncherSettings.Settings.call(
-                    context.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW,
-                    destDeviceState.getDbFile()).getBoolean(
-                    LauncherSettings.Settings.EXTRA_VALUE)) {
-                return false;
-            }
-        } else if (!LauncherSettings.Settings.call(
-                context.getContentResolver(),
-                LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER,
-                destDeviceState.getDbFile()).getBoolean(
-                LauncherSettings.Settings.EXTRA_VALUE)) {
-            return false;
-        }
-
         long migrationStartTime = System.currentTimeMillis();
-        try (SQLiteTransaction t = (SQLiteTransaction) LauncherSettings.Settings.call(
-                context.getContentResolver(),
-                LauncherSettings.Settings.METHOD_NEW_TRANSACTION).getBinder(
-                LauncherSettings.Settings.EXTRA_VALUE)) {
-
-            DbReader srcReader = new DbReader(t.getDb(),
-                    migrateForPreview ? LauncherSettings.Favorites.TABLE_NAME
-                            : LauncherSettings.Favorites.TMP_TABLE,
-                    context, validPackages);
-            DbReader destReader = new DbReader(t.getDb(),
-                    migrateForPreview ? LauncherSettings.Favorites.PREVIEW_TABLE_NAME
-                            : LauncherSettings.Favorites.TABLE_NAME,
-                    context, validPackages);
+        try (SQLiteTransaction t = new SQLiteTransaction(target.getWritableDatabase())) {
+            DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context, validPackages);
+            DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context, validPackages);
 
             Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
-            migrate(context, t.getDb(), srcReader, destReader, destDeviceState.getNumHotseat(),
+            migrate(target, srcReader, destReader, destDeviceState.getNumHotseat(),
                     targetSize, srcDeviceState, destDeviceState);
-
-            if (!migrateForPreview) {
-                dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
-            }
-
+            dropTable(t.getDb(), TMP_TABLE);
             t.commit();
             return true;
         } catch (Exception e) {
@@ -174,7 +133,7 @@
             Log.v(TAG, "Workspace migration completed in "
                     + (System.currentTimeMillis() - migrationStartTime));
 
-            if (!migrateForPreview) {
+            if (!(context instanceof SandboxContext)) {
                 // Save current configuration, so that the migration does not run again.
                 destDeviceState.writeToPrefs(context);
             }
@@ -182,7 +141,7 @@
     }
 
     public static boolean migrate(
-            @NonNull final Context context, @NonNull final SQLiteDatabase db,
+            @NonNull DatabaseHelper helper,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
             final int destHotseatSize, @NonNull final Point targetSize,
             @NonNull final DeviceGridState srcDeviceState,
@@ -234,8 +193,8 @@
         Collections.sort(workspaceToBeAdded);
 
         // Migrate hotseat
-        solveHotseatPlacement(db, srcReader,
-                destReader, context, destHotseatSize, dstHotseatItems, hotseatToBeAdded);
+        solveHotseatPlacement(helper, destHotseatSize,
+                srcReader, destReader, dstHotseatItems, hotseatToBeAdded);
 
         // Migrate workspace.
         // First we create a collection of the screens
@@ -255,8 +214,8 @@
             if (DEBUG) {
                 Log.d(TAG, "Migrating " + screenId);
             }
-            solveGridPlacement(db, srcReader,
-                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, false);
+            solveGridPlacement(helper, srcReader,
+                    destReader, screenId, trgX, trgY, workspaceToBeAdded, false);
             if (workspaceToBeAdded.isEmpty()) {
                 break;
             }
@@ -266,8 +225,8 @@
         // any of the screens, in this case we add them to new screens until all of them are placed.
         int screenId = destReader.mLastScreenId + 1;
         while (!workspaceToBeAdded.isEmpty()) {
-            solveGridPlacement(db, srcReader,
-                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
+            solveGridPlacement(helper, srcReader,
+                    destReader, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
             screenId++;
         }
 
@@ -298,33 +257,33 @@
         });
     }
 
-    private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry,
+    private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
             String srcTableName, String destTableName) {
-        int id = copyEntryAndUpdate(db, context, entry, srcTableName, destTableName);
+        int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
 
         if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
                 for (int itemId : itemIds) {
-                    copyEntryAndUpdate(db, context, itemId, id, srcTableName, destTableName);
+                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
                 }
             }
         }
     }
 
-    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+    private static int copyEntryAndUpdate(DatabaseHelper helper,
             DbEntry entry, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(db, context, entry, -1, -1, srcTableName, destTableName);
+        return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName);
     }
 
-    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+    private static int copyEntryAndUpdate(DatabaseHelper helper,
             int id, int folderId, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(db, context, null, id, folderId, srcTableName, destTableName);
+        return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName);
     }
 
-    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
-            DbEntry entry, int id, int folderId, String srcTableName, String destTableName) {
+    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry,
+            int id, int folderId, String srcTableName, String destTableName) {
         int newId = -1;
-        Cursor c = db.query(srcTableName, null,
+        Cursor c = helper.getWritableDatabase().query(srcTableName, null,
                 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
                 null, null, null, null);
         while (c.moveToNext()) {
@@ -335,11 +294,9 @@
             } else {
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
-            newId = LauncherSettings.Settings.call(context.getContentResolver(),
-                    LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
-                    LauncherSettings.Settings.EXTRA_VALUE);
+            newId = helper.generateNewItemId();
             values.put(LauncherSettings.Favorites._ID, newId);
-            db.insert(destTableName, null, values);
+            helper.getWritableDatabase().insert(destTableName, null, values);
         }
         c.close();
         return newId;
@@ -367,9 +324,9 @@
         return validPackages;
     }
 
-    private static void solveGridPlacement(@NonNull final SQLiteDatabase db,
+    private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
-            @NonNull final Context context, final int screenId, final int trgX, final int trgY,
+            final int screenId, final int trgX, final int trgY,
             @NonNull final List<DbEntry> sortedItemsToPlace, final boolean matchingScreenIdOnly) {
         final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
         final Point trg = new Point(trgX, trgY);
@@ -391,7 +348,7 @@
                 continue;
             }
             if (findPlacementForEntry(entry, next, trg, occupied, screenId)) {
-                insertEntryInDb(db, context, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
                 iterator.remove();
             }
         }
@@ -428,9 +385,9 @@
         return false;
     }
 
-    private static void solveHotseatPlacement(@NonNull final SQLiteDatabase db,
+    private static void solveHotseatPlacement(
+            @NonNull final DatabaseHelper helper, final int hotseatSize,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
-            @NonNull final Context context, final int hotseatSize,
             @NonNull final  List<DbEntry> placedHotseatItems,
             @NonNull final List<DbEntry> itemsToPlace) {
 
@@ -447,7 +404,7 @@
                 // to something other than -1.
                 entry.cellX = i;
                 entry.cellY = 0;
-                insertEntryInDb(db, context, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
                 occupied[entry.screenId] = true;
             }
         }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index a5dccc1..2054d93 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -16,16 +16,16 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
+
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.database.CursorWrapper;
-import android.net.Uri;
 import android.os.UserHandle;
 import android.provider.BaseColumns;
 import android.text.TextUtils;
@@ -66,9 +66,7 @@
     private final LongSparseArray<UserHandle> allUsers;
 
     private final LauncherAppState mApp;
-    private final Uri mContentUri;
     private final Context mContext;
-    private final PackageManager mPM;
     private final IconCache mIconCache;
     private final InvariantDeviceProfile mIDP;
 
@@ -108,17 +106,14 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor cursor, Uri contentUri, LauncherAppState app,
-            UserManagerState userManagerState) {
+    public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState) {
         super(cursor);
 
         mApp = app;
         allUsers = userManagerState.allUsers;
-        mContentUri = contentUri;
         mContext = app.getContext();
         mIconCache = app.getIconCache();
         mIDP = app.getInvariantDeviceProfile();
-        mPM = mContext.getPackageManager();
 
         // Init column indices
         mIconIndex = getColumnIndexOrThrow(Favorites.ICON);
@@ -390,7 +385,7 @@
      */
     public ContentWriter updater() {
        return new ContentWriter(mContext, new ContentWriter.CommitParams(
-               mApp.getModel().getModelDbController().getDatabaseHelper(),
+               mApp.getModel().getModelDbController(),
                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
     }
 
@@ -409,8 +404,8 @@
     public boolean commitDeleted() {
         if (mItemsToRemove.size() > 0) {
             // Remove dead items
-            mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery(
-                    Favorites._ID, mItemsToRemove), null);
+            mApp.getModel().getModelDbController().delete(TABLE_NAME,
+                    Utilities.createDbSelectionQuery(Favorites._ID, mItemsToRemove), null);
             return true;
         }
         return false;
@@ -435,9 +430,8 @@
             // Update restored items that no longer require special handling
             ContentValues values = new ContentValues();
             values.put(Favorites.RESTORED, 0);
-            mContext.getContentResolver().update(mContentUri, values,
-                    Utilities.createDbSelectionQuery(
-                            Favorites._ID, mRestoredRows), null);
+            mApp.getModel().getModelDbController().update(TABLE_NAME, values,
+                    Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null);
         }
     }
 
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 481cc6e..d4eded5 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_HAS_SHORTCUT_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_CHANGE_PERMISSION;
 import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_QUIET_MODE_ENABLED;
@@ -41,7 +42,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.graphics.Point;
-import android.net.Uri;
 import android.os.Bundle;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -50,7 +50,6 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
-import android.util.TimingLogger;
 
 import androidx.annotation.Nullable;
 
@@ -200,25 +199,10 @@
         }
 
         Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
-        TimingLogger timingLogger = new TimingLogger(TAG, "run");
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
-            Trace.beginSection("LoadWorkspace");
-            try {
-                loadWorkspace(allShortcuts, memoryLogger);
-            } finally {
-                Trace.endSection();
-            }
-            logASplit(timingLogger, "loadWorkspace");
-
-            if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
-                verifyNotStopped();
-                mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
-                        mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                mModelDelegate.markActive();
-                logASplit(timingLogger, "workspaceDelegateItems");
-            }
+            loadWorkspace(allShortcuts, "", memoryLogger);
 
             // Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
             // sanitizeData should not be invoked if the workspace is loaded from a db different
@@ -228,21 +212,21 @@
                 verifyNotStopped();
                 sanitizeFolders(mItemsDeleted);
                 sanitizeWidgetsShortcutsAndPackages();
-                logASplit(timingLogger, "sanitizeData");
+                logASplit("sanitizeData");
             }
 
             verifyNotStopped();
-            mLauncherBinder.bindWorkspace(true /* incrementBindId */);
-            logASplit(timingLogger, "bindWorkspace");
+            mLauncherBinder.bindWorkspace(true /* incrementBindId */, /* isBindSync= */ false);
+            logASplit("bindWorkspace");
 
             mModelDelegate.workspaceLoadComplete();
             // Notify the installer packages of packages with active installs on the first screen.
             sendFirstScreenActiveInstallsBroadcast();
-            logASplit(timingLogger, "sendFirstScreenActiveInstallsBroadcast");
+            logASplit("sendFirstScreenActiveInstallsBroadcast");
 
             // Take a break
             waitForIdle();
-            logASplit(timingLogger, "step 1 complete");
+            logASplit("step 1 complete");
             verifyNotStopped();
 
             // second step
@@ -253,16 +237,16 @@
             } finally {
                 Trace.endSection();
             }
-            logASplit(timingLogger, "loadAllApps");
+            logASplit("loadAllApps");
 
             if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
                         mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit(timingLogger, "allAppsDelegateItems");
+                logASplit("allAppsDelegateItems");
             }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
-            logASplit(timingLogger, "bindAllApps");
+            logASplit("bindAllApps");
 
             verifyNotStopped();
             IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
@@ -270,75 +254,73 @@
             updateHandler.updateIcons(allActivityList,
                     LauncherActivityCachingLogic.newInstance(mApp.getContext()),
                     mApp.getModel()::onPackageIconsUpdated);
-            logASplit(timingLogger, "update icon cache");
+            logASplit("update icon cache");
 
             verifyNotStopped();
-            logASplit(timingLogger, "save shortcuts in icon cache");
+            logASplit("save shortcuts in icon cache");
             updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
                     mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
             waitForIdle();
-            logASplit(timingLogger, "step 2 complete");
+            logASplit("step 2 complete");
             verifyNotStopped();
 
             // third step
             List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
-            logASplit(timingLogger, "loadDeepShortcuts");
+            logASplit("loadDeepShortcuts");
 
             verifyNotStopped();
             mLauncherBinder.bindDeepShortcuts();
-            logASplit(timingLogger, "bindDeepShortcuts");
+            logASplit("bindDeepShortcuts");
 
             verifyNotStopped();
-            logASplit(timingLogger, "save deep shortcuts in icon cache");
+            logASplit("save deep shortcuts in icon cache");
             updateHandler.updateIcons(allDeepShortcuts,
                     new ShortcutCachingLogic(), (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
-            logASplit(timingLogger, "step 3 complete");
+            logASplit("step 3 complete");
             verifyNotStopped();
 
             // fourth step
             List<ComponentWithLabelAndIcon> allWidgetsList =
                     mBgDataModel.widgetsModel.update(mApp, null);
-            logASplit(timingLogger, "load widgets");
+            logASplit("load widgets");
 
             verifyNotStopped();
             mLauncherBinder.bindWidgets();
-            logASplit(timingLogger, "bindWidgets");
+            logASplit("bindWidgets");
             verifyNotStopped();
 
             if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit(timingLogger, "otherDelegateItems");
+                logASplit("otherDelegateItems");
                 verifyNotStopped();
             }
 
             updateHandler.updateIcons(allWidgetsList,
                     new ComponentWithIconCachingLogic(mApp.getContext(), true),
                     mApp.getModel()::onWidgetLabelsUpdated);
-            logASplit(timingLogger, "save widgets in icon cache");
+            logASplit("save widgets in icon cache");
 
             // fifth step
             loadFolderNames();
 
             verifyNotStopped();
             updateHandler.finish();
-            logASplit(timingLogger, "finish icon update");
+            logASplit("finish icon update");
 
             mModelDelegate.modelLoadComplete();
             transaction.commit();
             memoryLogger.clearLogs();
         } catch (CancellationException e) {
             // Loader stopped, ignore
-            logASplit(timingLogger, "Cancelled");
+            logASplit("Cancelled");
         } catch (Exception e) {
             memoryLogger.printLogs();
             throw e;
-        } finally {
-            timingLogger.dumpToLog();
         }
         TraceHelper.INSTANCE.endSection(traceToken);
     }
@@ -348,25 +330,29 @@
         this.notify();
     }
 
-    private void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts, LoaderMemoryLogger memoryLogger) {
-        loadWorkspace(allDeepShortcuts, Favorites.CONTENT_URI,
-                null /* selection */, memoryLogger);
-    }
+    protected void loadWorkspace(
+            List<ShortcutInfo> allDeepShortcuts,
+            String selection,
+            LoaderMemoryLogger memoryLogger) {
+        Trace.beginSection("LoadWorkspace");
+        try {
+            loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger);
+        } finally {
+            Trace.endSection();
+        }
+        logASplit("loadWorkspace");
 
-    protected void loadWorkspaceForPreviewSurfaceRenderer(
-            List<ShortcutInfo> allDeepShortcuts, Uri contentUri, String selection) {
-        loadWorkspace(allDeepShortcuts, contentUri, selection, null);
         if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
+            verifyNotStopped();
             mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
                     mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
             mModelDelegate.markActive();
+            logASplit("workspaceDelegateItems");
         }
     }
 
-    protected void loadWorkspace(
+    private void loadWorkspaceImpl(
             List<ShortcutInfo> allDeepShortcuts,
-            Uri contentUri,
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger) {
         final Context context = mApp.getContext();
@@ -377,7 +363,7 @@
         final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
 
         boolean clearDb = false;
-        if (!GridSizeMigrationUtil.migrateGridIfNeeded(context)) {
+        if (!mApp.getModel().getModelDbController().migrateGridIfNeeded()) {
             // Migration failed. Clear workspace.
             clearDb = true;
         }
@@ -402,8 +388,9 @@
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
+            ModelDbController dbController = mApp.getModel().getModelDbController();
             final LoaderCursor c = new LoaderCursor(
-                    contentResolver.query(contentUri, null, selection, null, null), contentUri,
+                    dbController.query(TABLE_NAME, null, selection, null, null),
                     mApp, mUserManagerState);
             final Bundle extras = c.getExtras();
             mDbName = extras == null ? null : extras.getString(Settings.EXTRA_DB_NAME);
@@ -1112,12 +1099,9 @@
         FileLog.d(TAG, widgetDimension.toString());
     }
 
-    private static void logASplit(@Nullable TimingLogger timingLogger, String label) {
-        if (timingLogger != null) {
-            timingLogger.addSplit(label);
-            if (DEBUG) {
-                Log.d(TAG, label);
-            }
+    private static void logASplit(String label) {
+        if (DEBUG) {
+            Log.d(TAG, label);
         }
     }
 }
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 97bce8c..1274c08 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -19,11 +19,10 @@
 import static android.util.Base64.NO_WRAP;
 
 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
+import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
-import static com.android.launcher3.model.DatabaseHelper.EMPTY_DATABASE_CREATED;
-import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 
 import android.app.blob.BlobHandle;
@@ -31,7 +30,6 @@
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
@@ -43,6 +41,7 @@
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.text.TextUtils;
@@ -54,18 +53,22 @@
 
 import com.android.launcher3.AutoInstallsLayout;
 import com.android.launcher3.AutoInstallsLayout.SourceResources;
+import com.android.launcher3.ConstantItem;
 import com.android.launcher3.DefaultLayoutParser;
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherFiles;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.provider.LauncherDbUtils;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
 import com.android.launcher3.provider.RestoreDbTask;
 import com.android.launcher3.util.IOUtils;
 import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.util.Partner;
 import com.android.launcher3.widget.LauncherWidgetHolder;
 
@@ -73,7 +76,6 @@
 
 import java.io.InputStream;
 import java.io.StringReader;
-import java.util.function.Supplier;
 
 /**
  * Utility class which maintains an instance of Launcher database and provides utility methods
@@ -82,6 +84,8 @@
 public class ModelDbController {
     private static final String TAG = "LauncherProvider";
 
+    private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
+
     protected DatabaseHelper mOpenHelper;
 
     private final Context mContext;
@@ -92,26 +96,36 @@
 
     private synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = DatabaseHelper.createDatabaseHelper(
-                    mContext, false /* forMigration */);
-
-            RestoreDbTask.restoreIfNeeded(mContext, mOpenHelper);
+            mOpenHelper = createDatabaseHelper(false /* forMigration */);
+            RestoreDbTask.restoreIfNeeded(mContext, this);
         }
     }
 
-    private synchronized boolean prepForMigration(String dbFile, String targetTableName,
-            Supplier<DatabaseHelper> src, Supplier<DatabaseHelper> dst) {
-        if (TextUtils.equals(dbFile, mOpenHelper.getDatabaseName())) {
-            Log.e(TAG, "prepForMigration - target db is same as current: " + dbFile);
-            return false;
-        }
+    protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
+        boolean isSandbox = mContext instanceof SandboxContext;
+        String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
 
-        final DatabaseHelper helper = src.get();
-        mOpenHelper = dst.get();
-        copyTable(helper.getReadableDatabase(), Favorites.TABLE_NAME,
-                mOpenHelper.getWritableDatabase(), targetTableName, mContext);
-        helper.close();
-        return true;
+        // Set the flag for empty DB
+        Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
+                : () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbName).to(true));
+
+        DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbName,
+                this::getSerialNumberForUser, onEmptyDbCreateCallback);
+        // Table creation sometimes fails silently, which leads to a crash loop.
+        // This way, we will try to create a table every time after crash, so the device
+        // would eventually be able to recover.
+        if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
+            Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
+            // This operation is a no-op if the table already exists.
+            addTableToDb(databaseHelper.getWritableDatabase(),
+                    getSerialNumberForUser(Process.myUserHandle()),
+                    true /* optional */);
+        }
+        databaseHelper.mHotseatRestoreTableExists = tableExists(
+                databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
+
+        databaseHelper.initIds();
+        return databaseHelper;
     }
 
     /**
@@ -267,42 +281,38 @@
     }
 
     /**
-     * Updates the current DB and copies all the existing data to the temp table
-     * @param dbFile name of the target db file name
+     * Migrates the DB if needed, and returns false if the migration failed
+     * and DB needs to be cleared.
      */
-    @WorkerThread
-    public boolean updateCurrentOpenHelper(String dbFile) {
-        createDbIfNotExists();
-        return prepForMigration(
-                dbFile,
-                Favorites.TMP_TABLE,
-                () -> mOpenHelper,
-                () -> DatabaseHelper.createDatabaseHelper(
-                        mContext, true /* forMigration */));
+    public boolean migrateGridIfNeeded() {
+        InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
+        if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
+            return true;
+        }
+        String targetDbName = new DeviceGridState(idp).getDbFile();
+        if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
+            // DB is same, ignore
+            return true;
+        }
+        DatabaseHelper oldHelper = mOpenHelper;
+        mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
+                : createDatabaseHelper(true /* forMigration */);
+        try {
+            return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, idp, mOpenHelper,
+                   oldHelper.getWritableDatabase());
+        } finally {
+            if (mOpenHelper != oldHelper) {
+                oldHelper.close();
+            }
+        }
     }
 
     /**
-     * Returns the current DatabaseHelper.
-     * Only for tests
+     * Returns the underlying model database
      */
-    @WorkerThread
-    public DatabaseHelper getDatabaseHelper() {
+    public SQLiteDatabase getDb() {
         createDbIfNotExists();
-        return mOpenHelper;
-    }
-
-    /**
-     * Prepares the DB for preview by copying all existing data to preview table
-     */
-    @WorkerThread
-    public boolean prepareForPreview(String dbFile) {
-        createDbIfNotExists();
-        return prepForMigration(
-                dbFile,
-                Favorites.PREVIEW_TABLE_NAME,
-                () -> DatabaseHelper.createDatabaseHelper(
-                        mContext, dbFile, true /* forMigration */),
-                () -> mOpenHelper);
+        return mOpenHelper.getWritableDatabase();
     }
 
     private void onAddOrDeleteOp(SQLiteDatabase db) {
@@ -345,8 +355,7 @@
     }
 
     private void clearFlagEmptyDbCreated() {
-        LauncherPrefs.getPrefs(mContext).edit()
-                .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
+        LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()));
     }
 
     /**
@@ -359,9 +368,8 @@
     @WorkerThread
     public synchronized void loadDefaultFavoritesIfNecessary() {
         createDbIfNotExists();
-        SharedPreferences sp = LauncherPrefs.getPrefs(mContext);
 
-        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
+        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()))) {
             Log.d(TAG, "loading default workspace");
 
             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
@@ -479,4 +487,27 @@
         return new DefaultLayoutParser(mContext, widgetHolder,
                 mOpenHelper, mContext.getResources(), defaultLayout);
     }
+
+    /**
+     * Re-composite given key in respect to database. If the current db is
+     * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
+     * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
+     * string will be "EMPTY_DATABASE_CREATED@minimal.db".
+     */
+    private ConstantItem<Boolean> getEmptyDbCreatedKey(String dbName) {
+        if (mContext instanceof SandboxContext) {
+            return LauncherPrefs.nonRestorableItem(EMPTY_DATABASE_CREATED,
+                    false /* default value */, false /* boot aware */);
+        }
+        String key = TextUtils.equals(dbName, LauncherFiles.LAUNCHER_DB)
+                ? EMPTY_DATABASE_CREATED : EMPTY_DATABASE_CREATED + "@" + dbName;
+        return LauncherPrefs.backedUpItem(key, false /* default value */, false /* boot aware */);
+    }
+
+    /**
+     * Returns the serial number for the provided user
+     */
+    public long getSerialNumberForUser(UserHandle user) {
+        return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
+    }
 }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 48969fc..c718dcc 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -101,7 +101,7 @@
         UserManagerState ums = new UserManagerState();
         ums.init(UserCache.INSTANCE.get(context),
                 context.getSystemService(UserManager.class));
-        LoaderCursor lc = new LoaderCursor(c, null, LauncherAppState.getInstance(context), ums);
+        LoaderCursor lc = new LoaderCursor(c, LauncherAppState.getInstance(context), ums);
         IntSet deletedShortcuts = new IntSet();
 
         while (lc.moveToNext()) {
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index ac72164..a6e064a 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.provider;
 
+import static android.os.Process.myUserHandle;
+
 import static com.android.launcher3.InvariantDeviceProfile.TYPE_MULTI_DISPLAY;
 import static com.android.launcher3.LauncherPrefs.APP_WIDGET_IDS;
 import static com.android.launcher3.LauncherPrefs.OLD_APP_WIDGET_IDS;
@@ -47,8 +49,8 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.model.DeviceGridState;
+import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -83,12 +85,12 @@
     /**
      * Tries to restore the backup DB if needed
      */
-    public static void restoreIfNeeded(Context context, DatabaseHelper helper) {
+    public static void restoreIfNeeded(Context context, ModelDbController dbController) {
         if (!isPending(context)) {
             return;
         }
-        if (!performRestore(context, helper)) {
-            helper.createEmptyDB(helper.getWritableDatabase());
+        if (!performRestore(context, dbController)) {
+            dbController.createEmptyDB();
         }
 
         // Obtain InvariantDeviceProfile first before setting pending to false, so
@@ -102,12 +104,12 @@
         idp.reinitializeAfterRestore(context);
     }
 
-    private static boolean performRestore(Context context, DatabaseHelper helper) {
-        SQLiteDatabase db = helper.getWritableDatabase();
+    private static boolean performRestore(Context context, ModelDbController controller) {
+        SQLiteDatabase db = controller.getDb();
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
-            task.sanitizeDB(context, helper, db, new BackupManager(context));
-            task.restoreAppWidgetIdsIfExists(context, helper);
+            task.sanitizeDB(context, controller, db, new BackupManager(context));
+            task.restoreAppWidgetIdsIfExists(context, controller);
             t.commit();
             return true;
         } catch (Exception e) {
@@ -129,10 +131,10 @@
      * @return number of items deleted.
      */
     @VisibleForTesting
-    protected int sanitizeDB(Context context, DatabaseHelper helper, SQLiteDatabase db,
+    protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
             BackupManager backupManager) throws Exception {
         // Primary user ids
-        long myProfileId = helper.getDefaultUserSerial();
+        long myProfileId = controller.getSerialNumberForUser(myUserHandle());
         long oldProfileId = getDefaultProfileId(db);
         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
@@ -144,7 +146,7 @@
             long oldManagedProfileId = oldManagedProfileIds.keyAt(i);
             UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId);
             if (user != null) {
-                long newManagedProfileId = helper.getSerialNumberForUser(user);
+                long newManagedProfileId = controller.getSerialNumberForUser(user);
                 profileMapping.put(oldManagedProfileId, newManagedProfileId);
             }
         }
@@ -213,7 +215,7 @@
         }
 
         // Override shortcuts
-        maybeOverrideShortcuts(context, helper, db, myProfileId);
+        maybeOverrideShortcuts(context, controller, db, myProfileId);
 
         return itemsDeleted;
     }
@@ -321,11 +323,11 @@
                 .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
     }
 
-    private void restoreAppWidgetIdsIfExists(Context context, DatabaseHelper helper) {
+    private void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller) {
         LauncherPrefs lp = LauncherPrefs.get(context);
         if (lp.has(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS)) {
             AppWidgetHost host = new AppWidgetHost(context, APPWIDGET_HOST_ID);
-            AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, helper,
+            AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, controller,
                     IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(),
                     IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
                     host);
@@ -343,7 +345,7 @@
                 APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
     }
 
-    protected static void maybeOverrideShortcuts(Context context, DatabaseHelper helper,
+    protected static void maybeOverrideShortcuts(Context context, ModelDbController controller,
             SQLiteDatabase db, long currentUser) {
         Map<String, LauncherActivityInfo> activityOverrides = ApiWrapper.getActivityOverrides(
                 context);
@@ -367,7 +369,7 @@
                 if (override != null) {
                     ContentValues values = new ContentValues();
                     values.put(Favorites.PROFILE_ID,
-                            helper.getSerialNumberForUser(override.getUser()));
+                            controller.getSerialNumberForUser(override.getUser()));
                     values.put(Favorites.INTENT, AppInfo.makeLaunchIntent(override).toUri(0));
                     db.update(Favorites.TABLE_NAME, values, String.format("%s=?", Favorites._ID),
                             new String[]{String.valueOf(c.getInt(idIndex))});
diff --git a/src/com/android/launcher3/util/ContentWriter.java b/src/com/android/launcher3/util/ContentWriter.java
index e509235..7c5ef4d 100644
--- a/src/com/android/launcher3/util/ContentWriter.java
+++ b/src/com/android/launcher3/util/ContentWriter.java
@@ -26,7 +26,7 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.GraphicsUtils;
-import com.android.launcher3.model.DatabaseHelper;
+import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.pm.UserCache;
 
 /**
@@ -106,7 +106,7 @@
 
     public int commit() {
         if (mCommitParams != null) {
-            mCommitParams.mDatabaseHelper.getWritableDatabase().update(
+            mCommitParams.mDbController.update(
                     Favorites.TABLE_NAME, getValues(mContext),
                     mCommitParams.mWhere, mCommitParams.mSelectionArgs);
         }
@@ -115,12 +115,12 @@
 
     public static final class CommitParams {
 
-        final DatabaseHelper mDatabaseHelper;
+        final ModelDbController mDbController;
         final String mWhere;
         final String[] mSelectionArgs;
 
-        public CommitParams(DatabaseHelper helper, String where, String[] selectionArgs) {
-            mDatabaseHelper = helper;
+        public CommitParams(ModelDbController controller, String where, String[] selectionArgs) {
+            mDbController = controller;
             mWhere = where;
             mSelectionArgs = selectionArgs;
         }
diff --git a/src/com/android/launcher3/util/LockedUserState.kt b/src/com/android/launcher3/util/LockedUserState.kt
index f5e13d2..1231604 100644
--- a/src/com/android/launcher3/util/LockedUserState.kt
+++ b/src/com/android/launcher3/util/LockedUserState.kt
@@ -7,6 +7,7 @@
 import androidx.annotation.VisibleForTesting
 
 class LockedUserState(private val mContext: Context) : SafeCloseable {
+    val isUserUnlockedAtLauncherStartup: Boolean
     var isUserUnlocked: Boolean
         private set
     private val mUserUnlockedActions: RunnableList = RunnableList()
@@ -20,10 +21,17 @@
     }
 
     init {
+        // 1) when user reboots devices, launcher process starts at lock screen and both
+        // isUserUnlocked and isUserUnlockedAtLauncherStartup are init as false. After user unlocks
+        // screen, isUserUnlocked will be updated to true via Intent.ACTION_USER_UNLOCKED,
+        // yet isUserUnlockedAtLauncherStartup will remains as false.
+        // 2) when launcher process restarts after user has unlocked screen, both variable are
+        // init as true and will not change.
         isUserUnlocked =
             mContext
                 .getSystemService(UserManager::class.java)!!
                 .isUserUnlocked(Process.myUserHandle())
+        isUserUnlockedAtLauncherStartup = isUserUnlocked
         if (isUserUnlocked) {
             notifyUserUnlocked()
         } else {
diff --git a/tests/assets/ReorderAlgorithmUnitTest/reorder_algorithm_test_cases b/tests/assets/ReorderAlgorithmUnitTest/reorder_algorithm_test_cases
new file mode 100644
index 0000000..6f92d6c
--- /dev/null
+++ b/tests/assets/ReorderAlgorithmUnitTest/reorder_algorithm_test_cases
@@ -0,0 +1,1804 @@
+###################################################################################################
+# This file contains test case composed of the following tags:
+#     * # (coments): Lines starting with this character would be ignored.
+#     * arguments: is set of words separated by spaces that can later be parsed
+#     * board: represent a workspace, the first line is the dimensions of the board width x height (wxh)
+# There are different characters on the board that represent different things:
+#     * x: The x character represents spaces that would be ignored, for example it can be used in
+#          the first row if we don't know how wide the smartspace is.
+#     * i: Represents an icon on the workspace, none in particular just an icon
+#     * [a-z]: Represents a widget and it can be any number or character
+#          except any other already in use. The whole continuos are of the same character is the
+#          area of the widget.
+#     * [A-Z]: Represents a folder and number of icons in the folder is represented by the order of
+#          letter in the alphabet, A=2, B=3, C=4 ... etc.
+#
+# The "arguments:" tag represents where you want to add an item, the format is:
+#   "x y spanX spanY minSpanX minSpanY type<widget/icon/folder> ifSolutionIsPossible<valid/invalid>"
+#
+# Test are parsed by CellLayoutTestCaseReader.java and boards are parsed by CellLayoutBoard.java
+###################################################################################################
+# Test 0
+board: 7x10
+-----de
+-----de
+-----di
+-----ii
+-----ii
+-----ci
+-----ci
+-----ii
+aaaaaai
+iibbbbi
+# This represents where you want to add an item, the format is "x y spanX spanY minSpanX minSpanY type<widget/icon/folder> ifSolutionIsPossible<valid/invalid>"
+arguments: 1 3 5 4 4 1 widget valid
+board: 7x10
+-----cb
+-----cb
+-----ci
+izzzzzi
+izzzzzi
+azzzzzi
+azzzzzi
+-----ii
+ddddddi
+iieeeei
+# Test 1
+board: 9x10
+aaaaaaaai
+i-----hhj
+c-----hhj
+c-----hhi
+c-----iii
+i-----iii
+beddddddi
+befffffii
+iifffffii
+iiggggggi
+arguments: 3 3 4 6 1 2 widget valid
+board: 9x10
+ggggggggi
+i-zzzzffe
+h-zzzzffe
+h-zzzzffi
+h-zzzziii
+i-zzzziii
+dcbbbbbbi
+dcaaaaaii
+iiaaaaaii
+iijjjjjji
+# Test 2
+board: 7x6
+aiii--f
+acci--f
+addi--f
+iebi--i
+iebiiii
+iiiiiii
+arguments: 1 1 1 1 1 1 icon valid
+board: 7x6
+aiii--f
+azeei-f
+abbi--f
+icdi--i
+icdiiii
+iiiiiii
+# Test 3
+board: 4x10
+-ibg
+-ibg
+-ibi
+-idf
+-cdf
+-cdi
+-cei
+-cei
+-iii
+aaai
+arguments: 2 1 1 5 1 1 widget valid
+board: 4x10
+if-c
+ifzc
+ifzi
+igzb
+egzb
+egzi
+e-di
+e-di
+-iii
+aaai
+# Test 4
+board: 8x8
+i---c---
+a---c---
+a---i---
+ibbbi---
+--------
+--------
+----i---
+ii--i---
+arguments: 1 1 3 6 1 4 widget valid
+board: 8x8
+i---c---
+azzzc---
+azzzi---
+izzzbbbi
+-zzz----
+-zzz----
+-zzzi---
+ii--i---
+# Test 5
+board: 10x8
+aaaaaaaaai
+bbbbbcciii
+---------f
+---------f
+---------f
+---------i
+iiddddddii
+iieeiiiiii
+arguments: 2 5 7 1 3 1 widget valid
+board: 10x8
+bbbbbbbbbi
+eeeeecciii
+---------a
+---------a
+---------a
+--zzzzzzzi
+iiddddddii
+iiffiiiiii
+# Test 6
+board: 8x7
+-------i
+bbcaaaah
+bbcaaaah
+bbcaaaai
+bbcddddi
+bbieeeii
+iiiffggi
+arguments: 5 1 1 2 1 1 icon valid
+board: 8x7
+-----z-i
+bbfaaaad
+bbfaaaad
+bbfaaaai
+bbfhhhhi
+bbigggii
+iiieecci
+# Test 7
+board: 10x3
+ii-------i
+----------
+-ii----iii
+arguments: 1 0 7 1 2 1 widget valid
+board: 10x3
+izzzzzzzii
+----------
+-ii----iii
+# Test 8
+board: 8x9
+aaaaaiii
+ibbbbbii
+-----ffi
+-----eei
+-----hhi
+-----ggi
+-----jji
+iiccccii
+iidddddi
+arguments: 2 2 5 2 3 1 widget valid
+board: 8x9
+fffffiii
+ihhhhhii
+bbzzzzzi
+jjzzzzzi
+-----ggi
+-----cci
+-----aai
+iiddddii
+iieeeeei
+# Test 9
+board: 4x8
+--ci
+--cd
+--cd
+--ci
+--ii
+aaai
+aaai
+bbbi
+arguments: 1 4 2 1 2 1 widget valid
+board: 4x8
+--bi
+--bc
+--bc
+--bi
+izzi
+aaai
+aaai
+dddi
+# Test 10
+board: 8x3
+i----cci
+iaaaaaai
+iiibbbbi
+arguments: 2 1 2 1 1 1 widget valid
+board: 8x3
+i-zz-cci
+ibbbbbbi
+iiiaaaai
+# Test 11
+board: 5x10
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+i---i
+iaaai
+arguments: 0 3 3 6 1 2 widget valid
+board: 5x10
+-----
+-----
+-----
+zzz--
+zzz--
+zzz--
+zzz--
+zzz--
+zzzii
+iaaai
+# Test 12
+board: 6x10
+aad-ii
+aad-ii
+aai-eg
+aac-eg
+aac-ei
+aac-ef
+aai-ef
+aai-ei
+aai-ii
+bbiiii
+arguments: 1 5 1 4 1 3 widget valid
+board: 6x10
+ccb-ii
+ccb-ii
+cci-fa
+cce-fa
+cce-fi
+ccezfg
+ccizfg
+ccizfi
+ccizii
+ddiiii
+# Test 13
+board: 10x10
+iaaaaaaiii
+i-------ii
+i-------ii
+i-------ei
+i-------ei
+b-------ei
+b-------ei
+b-------ii
+biccccccci
+iiddddddii
+arguments: 0 2 6 1 3 1 widget valid
+board: 10x10
+iaaaaaaiii
+i-------ii
+zzzzzzi-ii
+i-------ci
+i-------ci
+e-------ci
+e-------ci
+e-------ii
+eibbbbbbbi
+iiddddddii
+# Test 14
+board: 3x6
+--a
+--a
+--a
+--a
+--i
+iii
+arguments: 0 2 1 3 1 3 widget valid
+board: 3x6
+--a
+--a
+z-a
+z-a
+z-i
+iii
+# Test 15
+board: 6x7
+------
+-i---i
+-i---c
+ia---c
+ia---i
+ia---i
+iibbbi
+arguments: 2 0 2 3 2 1 widget valid
+board: 6x7
+--zz--
+-izz-i
+-izz-b
+ia---b
+ia---i
+ia---i
+iiccci
+# Test 16
+board: 8x6
+bbbiiddi
+icccccci
+aaaggiii
+eeeeeffi
+ii----ii
+iiihhhhi
+arguments: 3 3 2 1 1 1 widget valid
+board: 8x6
+ccciiffi
+ieeeeeei
+ggghhiii
+aaaaaddi
+ii-zz-ii
+iiibbbbi
+# Test 17
+board: 4x9
+---i
+---i
+---d
+---d
+---i
+---i
+ibbi
+iaai
+icci
+arguments: 0 2 1 6 1 1 widget valid
+board: 4x9
+i--i
+i--i
+z--d
+z--d
+z--i
+z--i
+zaai
+zbbi
+icci
+# Test 18
+board: 7x5
+iaaaaai
+----iii
+----iii
+iiibbbi
+iiiccii
+arguments: 2 0 4 1 3 1 widget valid
+board: 7x5
+ibbbbbi
+zzzziii
+----iii
+iiiccci
+iiiaaii
+# Test 19
+board: 6x4
+aaaaii
+bbi--i
+cci--i
+iiiddi
+arguments: 4 0 1 2 1 1 widget valid
+board: 6x4
+cccczi
+aai-zi
+bbi-ii
+iiiddi
+# Test 20
+board: 8x5
+bbiaeeei
+bbiaiffi
+cciddddi
+----hhii
+iigggggi
+arguments: 0 1 1 3 1 1 widget valid
+board: 8x5
+bbieccci
+bbieiddi
+z-iffffi
+zaa-ggii
+iihhhhhi
+# Test 21
+board: 5x4
+i--ii
+a--ii
+aicci
+iibbi
+arguments: 1 2 2 1 1 1 widget valid
+board: 5x4
+iaaii
+bi-ii
+bzz-i
+iicci
+# Test 22
+board: 4x10
+---i
+---d
+---d
+---d
+---i
+aaai
+aaai
+aaai
+bbbi
+ccci
+arguments: 2 0 1 1 1 1 icon valid
+board: 4x10
+--zi
+---d
+---d
+---d
+---i
+bbbi
+bbbi
+bbbi
+ccci
+aaai
+# Test 23
+board: 10x10
+----------
+----------
+----------
+----------
+----------
+----------
+----------
+----------
+----------
+--aaai---i
+arguments: 0 0 8 5 5 4 widget valid
+board: 10x10
+zzzzzzzz--
+zzzzzzzz--
+zzzzzzzz--
+zzzzzzzz--
+zzzzzzzz--
+----------
+----------
+----------
+----------
+--aaai---i
+# Test 24
+board: 4x9
+---b
+---b
+---b
+---b
+---b
+---c
+---c
+---i
+iaai
+arguments: 2 0 1 2 1 1 widget valid
+board: 4x9
+--zc
+--zc
+---c
+---c
+---c
+---a
+---a
+---i
+ibbi
+# Test 25
+board: 4x5
+-cci
+-bbi
+-bbi
+-iii
+iaai
+arguments: 0 1 1 1 1 1 icon valid
+board: 4x5
+-bbi
+zaai
+-aai
+-iii
+icci
+# Test 26
+board: 7x3
+a-----i
+a-----i
+ibbbbbi
+arguments: 0 0 2 1 1 1 widget valid
+board: 7x3
+zza---i
+--a---i
+ibbbbbi
+# Test 27
+board: 8x6
+i-----di
+------di
+------ii
+--bbbbii
+i-iiccci
+i--iiaai
+arguments: 5 1 2 3 1 1 widget valid
+board: 8x6
+i---d--i
+----dzzi
+----izzi
+aaaaizzi
+i-iiccci
+i--iibbi
+# Test 28
+board: 4x7
+---i
+---i
+---a
+---a
+---a
+---i
+iiii
+arguments: 1 2 1 3 1 3 widget valid
+board: 4x7
+---i
+---i
+-z-a
+-z-a
+-z-a
+---i
+iiii
+# Test 29
+board: 5x10
+ii--b
+ii--b
+ai--i
+ai--i
+iiiii
+-----
+-----
+-----
+-----
+-----
+arguments: 1 2 2 5 1 4 widget valid
+board: 5x10
+ii--a
+ii--a
+bzz-i
+bzz-i
+izzii
+-zz--
+-zz--
+-i---
+-i---
+-ii--
+# Test 30
+board: 10x4
+a--------i
+a--------i
+i--------i
+ibbbbbicci
+arguments: 7 1 1 2 1 2 widget valid
+board: 10x4
+b--------i
+b------z-i
+i------z-i
+iaaaaaicci
+# Test 31
+board: 5x3
+a--ii
+a--ii
+iiiii
+arguments: 1 0 3 1 2 1 widget valid
+board: 5x3
+azzzi
+a-iii
+iiiii
+# Test 32
+board: 5x9
+-----
+-----
+-----
+-----
+----i
+----b
+----b
+----i
+aaaii
+arguments: 1 0 2 7 2 1 widget valid
+board: 5x9
+-zz--
+-zz--
+-zz--
+-zz--
+-zz-i
+-zz-a
+-zz-a
+----i
+bbbii
+# Test 33
+board: 4x8
+icci
+aaai
+aaai
+iiii
+ibbi
+---i
+---i
+dddi
+arguments: 2 1 1 6 1 5 icon invalid
+board: 4x8
+----
+----
+----
+----
+----
+----
+----
+----
+# Test 34
+board: 7x3
+------i
+------i
+iaaiiii
+arguments: 0 1 4 1 1 1 widget valid
+board: 7x3
+------i
+zzzz--i
+iaaiiii
+# Test 35
+board: 5x4
+ii-bi
+ai-bi
+ai-ii
+iiiii
+arguments: 1 1 2 1 2 1 widget valid
+board: 5x4
+iiiai
+bzzai
+bi-ii
+iiiii
+# Test 36
+board: 9x7
+iii--iiii
+aai--bbbi
+aai--bbbi
+aai--ccii
+iiiiiiiii
+---------
+---------
+arguments: 1 1 6 2 4 2 widget valid
+board: 9x7
+iii--iiii
+-zzzzzz-i
+-zzzzzz-i
+bbi--aaai
+bbiiiaaai
+bbi--cci-
+iii--iii-
+# Test 37
+board: 6x9
+aai--i
+-iii-i
+iiiiii
+-i----
+-ibbbi
+-ibbbi
+-iccci
+-i---i
+iidddi
+arguments: 2 2 3 1 2 1 widget valid
+board: 6x9
+ddi--i
+-iii-i
+iizzzi
+-iiii-
+-iaaai
+-iaaai
+-iccci
+-i---i
+iibbbi
+# Test 38
+board: 10x6
+----------
+----------
+----------
+----------
+----------
+----------
+arguments: 0 0 6 4 1 2 widget valid
+board: 10x6
+zzzzzz----
+zzzzzz----
+zzzzzz----
+zzzzzz----
+----------
+----------
+# Test 39
+board: 3x9
+-bc
+-bc
+-bd
+-bd
+-bi
+-bi
+-ii
+aai
+iii
+arguments: 0 1 1 6 1 4 widget valid
+board: 3x9
+-bd
+zbd
+zbc
+zbc
+zbi
+zbi
+zii
+aai
+iii
+# Test 40
+board: 3x3
+--i
+--i
+aai
+arguments: 1 0 1 1 1 1 icon valid
+board: 3x3
+-zi
+--i
+aai
+# Test 41
+board: 6x3
+-----i
+------
+--i---
+arguments: 0 1 4 1 3 1 widget valid
+board: 6x3
+-----i
+zzzz--
+--i---
+# Test 42
+board: 5x5
+iaaai
+---ii
+iibbi
+iicci
+iiiii
+arguments: 1 0 1 2 1 2 icon invalid
+board: 5x5
+-----
+-----
+-----
+-----
+-----
+# Test 43
+board: 3x6
+--i
+--i
+--b
+--b
+--i
+aai
+arguments: 1 2 1 3 1 3 widget valid
+board: 3x6
+--i
+--i
+-za
+-za
+-zi
+bbi
+# Test 44
+board: 4x8
+---a
+---a
+---a
+---a
+---a
+---i
+iiii
+iiii
+arguments: 2 2 1 3 1 3 widget valid
+board: 4x8
+---a
+---a
+--za
+--za
+--za
+---i
+iiii
+iiii
+# Test 45
+board: 9x9
+i-----kki
+i-----kki
+a-----kki
+a-----kki
+i-----lli
+iicccibbj
+idiffibbj
+ideeiibbi
+iigggihhi
+arguments: 2 1 5 3 4 2 widget valid
+board: 9x9
+i-----lli
+izzzzzlli
+fzzzzzlli
+fzzzzzlli
+i-----jji
+iihhhikke
+igiccikke
+igbbiikki
+iiaaaiddi
+# Test 46
+board: 9x5
+ab---iggi
+ab---iffi
+ab---eeei
+aicccccci
+iiiidddii
+arguments: 1 0 2 3 1 3 widget valid
+board: 9x5
+bzze-iffi
+bzze-iaai
+bzze-dddi
+bicccccci
+iiiigggii
+# Test 47
+board: 5x6
+ai--b
+ai--b
+ai--i
+ai--i
+iiiii
+iiiii
+arguments: 2 4 2 1 2 1 widget valid
+board: 5x6
+bi--a
+bi--a
+bi--i
+biiii
+iizzi
+iiiii
+# Test 48
+board: 5x4
+-----
+-i---
+----i
+-iiii
+arguments: 1 0 3 2 2 1 widget valid
+board: 5x4
+-zzz-
+-zzzi
+----i
+-iiii
+# Test 49
+board: 6x3
+i---ii
+i---ii
+iiaaai
+arguments: 2 0 1 1 1 1 icon valid
+board: 6x3
+i-z-ii
+i---ii
+iiaaai
+# Test 50
+board: 8x9
+--------
+--------
+--------
+--------
+--------
+--------
+--------
+--------
+--------
+arguments: 1 1 6 7 3 1 widget valid
+board: 8x9
+--------
+-zzzzzz-
+-zzzzzz-
+-zzzzzz-
+-zzzzzz-
+-zzzzzz-
+-zzzzzz-
+-zzzzzz-
+--------
+# Test 51
+board: 3x6
+--b
+--b
+--b
+--i
+iii
+aai
+arguments: 0 3 1 2 1 2 widget valid
+board: 3x6
+--a
+--a
+i-a
+z-i
+zii
+bbi
+# Test 52
+board: 5x8
+a--df
+a--df
+a--di
+a--ei
+i--ei
+i--ii
+ibbbi
+iccci
+arguments: 1 3 1 3 1 2 widget valid
+board: 5x8
+e--cb
+e--cb
+e--ci
+ez-di
+iz-di
+iz-ii
+iaaai
+ifffi
+# Test 53
+board: 8x3
+aaaai--i
+iccci--i
+bbbiiiii
+arguments: 1 1 3 1 3 1 icon invalid
+board: 8x3
+--------
+--------
+--------
+# Test 54
+board: 8x9
+------di
+------di
+------di
+------ii
+iaaaaaai
+baaaaaai
+baaaaaai
+baaaaaai
+iiccccci
+arguments: 3 3 2 1 1 1 widget valid
+board: 8x9
+------ai
+------ai
+------ai
+---zz-ii
+icccccci
+bcccccci
+bcccccci
+bcccccci
+iidddddi
+# Test 55
+board: 4x8
+ibbi
+iaai
+iaai
+caai
+ciii
+iddi
+--ii
+ieei
+arguments: 1 4 1 1 1 1 icon valid
+board: 4x8
+iaai
+icci
+icci
+ecci
+ezii
+ibbi
+-iii
+iddi
+# Test 56
+board: 6x9
+-----i
+------
+-i---i
+i----i
+-----i
+--icci
+-i-bbi
+-i-iii
+iiaaai
+arguments: 1 0 4 2 4 2 widget valid
+board: 6x9
+-zzzzi
+-zzzz-
+-i---i
+i----i
+-----i
+--icci
+-i-bbi
+-i-iii
+iiaaai
+# Test 57
+board: 4x9
+aaai
+ibbi
+c-ii
+c-ei
+c-ei
+c-ii
+c-ii
+ciii
+iddi
+arguments: 1 6 2 2 2 2 widget valid
+board: 4x9
+eeei
+icci
+d-ii
+dibi
+dibi
+diii
+dzzi
+dzzi
+iaai
+# Test 58
+board: 3x6
+aai
+iii
+--b
+--b
+--i
+iii
+arguments: 0 1 1 1 1 1 icon valid
+board: 3x6
+aai
+zii
+i-b
+--b
+--i
+iii
+# Test 59
+board: 9x6
+iiiiii--i
+iaaeii--g
+baaeii--g
+baaiii--i
+icciiiiii
+iddiiiffi
+arguments: 3 3 5 1 4 1 widget valid
+board: 9x6
+iiiiii--i
+ibbdiii-a
+ebbdiiiia
+ebbzzzzzi
+icciiiiii
+iffiiiggi
+# Test 60
+board: 5x8
+iiiii
+ii-ii
+ai-ii
+ai-ii
+ai-ii
+aibbi
+iicci
+iiiii
+arguments: 2 0 2 2 1 2 widget valid
+board: 5x8
+iizzi
+iizzi
+biiii
+biiii
+biiii
+bicci
+iiaai
+iiiii
+# Test 61
+board: 7x10
+-------
+-------
+-------
+-------
+-------
+-------
+-------
+-------
+-------
+-------
+arguments: 2 0 3 8 2 7 widget valid
+board: 7x10
+--zzz--
+--zzz--
+--zzz--
+--zzz--
+--zzz--
+--zzz--
+--zzz--
+--zzz--
+-------
+-------
+# Test 62
+board: 8x8
+aaiiiiii
+iiibbbbi
+dddi---i
+iiii---i
+ccci---h
+ccci---h
+eeei---i
+fffigggi
+arguments: 1 4 4 2 1 1 widget valid
+board: 8x8
+eeiiiiii
+iiiaaaai
+fffigggi
+iiiigggi
+-zzzzi-c
+-zzzz--c
+hhhii--i
+dddibbbi
+# Test 63
+board: 8x5
+ibbii--d
+iiiii--d
+aaaii--d
+aaaii--i
+cccciiii
+arguments: 3 2 2 1 2 1 widget valid
+board: 8x5
+ibbii--d
+iiiii--d
+ccczziid
+cccii--i
+aaaaiiii
+# Test 64
+board: 9x5
+iiac--iii
+iiac--ffi
+biac--eei
+biai--iii
+iiiiddddi
+arguments: 3 1 2 3 2 1 widget valid
+board: 9x5
+iif--eiii
+iifzzebbi
+cifzzeaai
+cifzziiii
+iiiiddddi
+# Test 65
+board: 6x7
+aaaiii
+aaaiii
+ibbcci
+i----f
+d----f
+d----i
+iieeei
+arguments: 2 1 1 5 1 4 widget valid
+board: 6x7
+aaaiii
+aaaiii
+i-zbbi
+i-zeec
+d-z--c
+d-z--i
+iifffi
+# Test 66
+board: 10x10
+aaaaaaaaii
+i---ijjjni
+i---ijjjni
+b---kjjjni
+b---kjjjii
+b---klliii
+b---immmmi
+bcccccccci
+ieeiddiiii
+iffiigghhi
+arguments: 2 4 3 4 3 1 widget valid
+board: 10x10
+aaaaaaaaii
+i---innnci
+i---innnci
+hzzzknnnci
+hzzzknnnii
+hzzzkeeiii
+hzzziffffi
+hddddddddi
+ijjilliiii
+iggiimmbbi
+# Test 67
+board: 3x8
+--i
+--i
+--i
+bai
+bai
+iai
+iai
+iii
+arguments: 0 0 1 2 1 1 widget valid
+board: 3x8
+z-i
+z-i
+--i
+bai
+bai
+iai
+iai
+iii
+# Test 68
+board: 6x5
+i---ii
+ibcaei
+ibcaei
+ibiaii
+iiiddi
+arguments: 4 1 1 3 1 1 icon valid
+board: 6x5
+i--zii
+icadbi
+icadbi
+icidii
+iiieei
+# Test 69
+board: 3x7
+a-i
+a-b
+a-b
+a-b
+a-i
+i-i
+iii
+arguments: 0 0 1 4 1 1 widget valid
+board: 3x7
+zbi
+zba
+zba
+zba
+-bi
+i-i
+iii
+# Test 70
+board: 5x10
+---ii
+---ii
+---ii
+aaaai
+-----
+-----
+-----
+-----
+-----
+-----
+arguments: 0 5 3 4 3 1 widget valid
+board: 5x10
+---ii
+---ii
+---ii
+aaaai
+-----
+zzz--
+zzz--
+zzz--
+zzz--
+-----
+# Test 71
+board: 8x9
+-----i-i
+--------
+--------
+--------
+--------
+--------
+--------
+--------
+--------
+arguments: 2 3 5 5 1 4 widget valid
+board: 8x9
+-----i-i
+--------
+--------
+--zzzzz-
+--zzzzz-
+--zzzzz-
+--zzzzz-
+--zzzzz-
+--------
+# Test 72
+board: 4x6
+i--d
+a--d
+a--i
+acbi
+acbi
+iiii
+arguments: 0 2 1 3 1 2 widget valid
+board: 4x6
+iz-b
+cz-b
+cz-i
+cadi
+cadi
+iiii
+# Test 73
+board: 3x7
+--c
+--c
+--c
+--i
+bai
+bai
+iii
+arguments: 1 0 1 2 1 1 widget valid
+board: 3x7
+-zb
+-zb
+--b
+--i
+cai
+cai
+iii
+# Test 74
+board: 4x7
+---i
+---i
+---b
+---b
+---b
+---i
+iaai
+arguments: 2 2 1 3 1 1 widget valid
+board: 4x7
+---i
+---i
+--zb
+--zb
+--zb
+---i
+iaai
+# Test 75
+board: 4x3
+a--i
+a--i
+ibbi
+arguments: 1 0 1 1 1 1 icon valid
+board: 4x3
+bz-i
+b--i
+iaai
+# Test 76
+board: 10x5
+--ai------
+--aii----i
+--iii----i
+--iii----i
+iiiiiibbbi
+arguments: 5 2 4 2 3 2 widget valid
+board: 10x5
+--bi------
+--bii----i
+--iiizzzzi
+--iiizzzzi
+iiiiiiaaai
+# Test 77
+board: 8x10
+-------i
+-------e
+-------e
+-------e
+-------e
+-------e
+-------i
+ccibbbii
+iaaaaaai
+iiiddddi
+arguments: 4 2 3 5 3 1 widget valid
+board: 8x10
+-------i
+-------c
+----zzzc
+----zzzc
+----zzzc
+----zzzc
+----zzzi
+aaibbbii
+iddddddi
+iiieeeei
+# Test 78
+board: 3x9
+aai
+i-i
+iii
+--i
+---
+--i
+b-i
+bii
+iii
+arguments: 1 1 1 7 1 6 widget valid
+board: 3x9
+aai
+izi
+izi
+izi
+-z-
+izi
+bzi
+bzi
+iii
+# Test 79
+board: 6x5
+aaii-i
+--i---
+--i--i
+iii--i
+iiiiii
+arguments: 2 3 3 1 3 1 widget valid
+board: 6x5
+aaii-i
+--i---
+--ii-i
+iizzzi
+iiiiii
+# Test 80
+board: 6x4
+------
+------
+----ii
+------
+arguments: 0 0 4 2 3 1 widget valid
+board: 6x4
+zzzz--
+zzzz--
+----ii
+------
+# Test 81
+board: 5x6
+aaaai
+i--ii
+b--ii
+b--ii
+b--ii
+iiiii
+arguments: 2 3 2 1 1 1 widget valid
+board: 5x6
+aaaai
+i--ii
+b--ii
+bizzi
+b--ii
+iiiii
+# Test 82
+board: 6x8
+aaaaii
+bbbbbi
+icciii
+-----i
+-----i
+ddfffi
+ddgggi
+eeeeei
+arguments: 1 3 2 2 1 1 widget valid
+board: 6x8
+bbbbii
+gggggi
+icciii
+-zz--i
+-zz--i
+eedddi
+eefffi
+aaaaai
+# Test 83
+board: 8x10
+iiaaaaai
+------cd
+------cd
+------ci
+------ii
+iiiiibbi
+--------
+--------
+--------
+--------
+arguments: 2 0 5 3 5 1 widget valid
+board: 8x10
+iizzzzzi
+--zzzzza
+--zzzzza
+--dddddi
+------bi
+iiiii-bi
+------b-
+------i-
+-----cc-
+--------
+# Test 84
+board: 6x6
+---aii
+---aii
+---bbi
+---iii
+--iiii
+i----i
+arguments: 2 0 3 4 1 3 widget valid
+board: 6x6
+bizzzi
+bizzzi
+aazzzi
+iizzzi
+--iiii
+i----i
+# Test 85
+board: 4x7
+iaai
+ii-i
+ii-i
+ii-i
+iiii
+----
+---i
+arguments: 2 2 1 4 1 2 widget valid
+board: 4x7
+iaai
+ii-i
+iizi
+iizi
+iizi
+--z-
+--ii
+# Test 86
+board: 4x9
+----
+a--i
+a--i
+i--i
+i--c
+i--c
+b--i
+b--i
+iiii
+arguments: 0 0 2 7 2 7 widget valid
+board: 4x9
+zz--
+zzbi
+zzbi
+zzii
+zzia
+zzia
+zzci
+--ci
+iiii
+# Test 87
+board: 5x6
+-----
+-i--i
+-----
+-----
+-----
+-----
+arguments: 0 0 3 3 1 2 widget valid
+board: 5x6
+zzz--
+zzzii
+zzz--
+-----
+-----
+-----
+# Test 88
+board: 5x5
+ii--a
+ii--a
+-i--i
+-i-ii
+ii--i
+arguments: 0 1 2 3 2 1 widget valid
+board: 5x5
+iiiia
+zz-ia
+zz-ii
+zz-ii
+ii--i
+# Test 89
+board: 5x10
+ccddi
+bbbii
+eeeei
+iajji
+faiii
+fgggi
+fgggi
+ihhhi
+----i
+iikki
+arguments: 1 3 1 4 1 2 icon invalid
+board: 5x10
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+-----
+# Test 90
+board: 3x6
+--i
+--b
+--b
+--b
+--i
+aai
+arguments: 1 0 1 2 1 1 widget valid
+board: 3x6
+-zi
+-za
+--a
+--a
+--i
+bbi
+# Test 91
+board: 4x8
+---i
+---c
+---c
+---c
+---c
+---i
+aaai
+bbbi
+arguments: 1 1 1 2 1 2 widget valid
+board: 4x8
+---i
+-z-c
+-z-c
+---c
+---c
+---i
+bbbi
+aaai
+# Test 92
+board: 5x7
+----c
+----c
+----c
+----c
+----i
+aaaai
+ibbii
+arguments: 1 1 2 4 1 2 widget valid
+board: 5x7
+----b
+-zz-b
+-zz-b
+-zz-b
+-zz-i
+aaaai
+iccii
+# Test 93
+board: 5x4
+i--ii
+i--ii
+i--ii
+iiiii
+arguments: 2 1 1 2 1 2 widget valid
+board: 5x4
+i--ii
+i-zii
+i-zii
+iiiii
+# Test 94
+board: 10x5
+aabc---iii
+aabc---eei
+aabi---ffi
+aaii---ggi
+iiiiiddddi
+arguments: 4 1 3 3 1 3 widget valid
+board: 10x5
+ggfe---iii
+ggfezzzddi
+ggfizzzbbi
+ggiizzzaai
+iiiiicccci
+# Test 95
+board: 9x5
+baffi---i
+baeei---i
+caiii---i
+ciddi---i
+iiiiiggii
+arguments: 0 0 4 3 3 1 widget valid
+board: 9x5
+ezzzdccii
+ezzzdggii
+fzzzdiiii
+fibbi---i
+iiiiiaaii
+# Test 96
+board: 7x7
+-------
+--i---i
+-------
+-------
+------i
+-------
+------i
+arguments: 0 1 5 2 3 1 widget valid
+board: 7x7
+-------
+zzzzzii
+zzzzz--
+-------
+------i
+-------
+------i
+# Test 97
+board: 10x4
+i-----ccii
+a-----ccii
+a-----iddi
+iibbbiiiii
+arguments: 7 2 1 1 1 1 icon valid
+board: 10x4
+i-----ccii
+d-----ccii
+d---ibbz-i
+iiaaaiiiii
+# Test 98
+board: 7x9
+--i-iii
+ai-icci
+ai-iiii
+iibbbbi
+ii-iiii
+-i-ii-i
+-i-ii-i
+ii-iiii
+ii----i
+arguments: 1 6 3 2 3 2 widget valid
+board: 7x9
+--i-iii
+bi-iaai
+bi-iiii
+iicccci
+ii-iiii
+-iiii-i
+izzzi-i
+izzziii
+iiii--i
+# Test 99
+board: 4x10
+aaii
+iiii
+---c
+---c
+---c
+---d
+---d
+---i
+ibbi
+iiii
+arguments: 0 0 2 6 1 2 widget valid
+board: 4x10
+zzii
+zzii
+zz-c
+zz-c
+zz-c
+zz-b
+aa-b
+ii-i
+iddi
+iiii
+# Test 100
+board: 8x4
+iittt---
+yyyy---i
+yyyyi--i
+yyyy---i
+# This represents where you want to add an item, the format is "x y spanX spanY minSpanX minSpanY type<widget/icon/folder> ifSolutionIsPossible<valid/invalid>"
+arguments: 5 1 2 3 1 1 widget valid
+board: 8x4
+iittt---
+yyyy-hhi
+yyyyihhi
+yyyy-hhi
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
index cf96f04..3c2b49a 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutBoard.java
@@ -171,6 +171,8 @@
         }
     }
 
+    private HashSet<Character> mUsedWidgetTypes = new HashSet<>();
+
     static final int INFINITE = 99999;
 
     char[][] mWidget = new char[30][30];
@@ -182,6 +184,8 @@
 
     WidgetRect mMain = null;
 
+    int mWidth, mHeight;
+
     CellLayoutBoard() {
         for (int x = 0; x < mWidget.length; x++) {
             for (int y = 0; y < mWidget[0].length; y++) {
@@ -190,6 +194,17 @@
         }
     }
 
+    CellLayoutBoard(int width, int height) {
+        mWidget = new char[width][height];
+        this.mWidth = width;
+        this.mHeight = height;
+        for (int x = 0; x < mWidget.length; x++) {
+            for (int y = 0; y < mWidget[0].length; y++) {
+                mWidget[x][y] = CellType.EMPTY;
+            }
+        }
+    }
+
     public List<WidgetRect> getWidgets() {
         return mWidgetsRects;
     }
@@ -256,6 +271,16 @@
         }).collect(Collectors.toList());
     }
 
+    private char getNextWidgetType() {
+        for (char type = 'a'; type <= 'z'; type++) {
+            if (type == 'i') continue;
+            if (mUsedWidgetTypes.contains(type)) continue;
+            mUsedWidgetTypes.add(type);
+            return type;
+        }
+        return 'z';
+    }
+
     public void addWidget(int x, int y, int spanX, int spanY, char type) {
         Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y);
         removeOverlappingItems(rect);
@@ -268,6 +293,10 @@
         }
     }
 
+    public void addWidget(int x, int y, int spanX, int spanY) {
+        addWidget(x, y, spanX, spanY, getNextWidgetType());
+    }
+
     public void addIcon(int x, int y) {
         Point iconCoord = new Point(x, y);
         removeOverlappingItems(iconCoord);
@@ -367,6 +396,8 @@
                 }
             }
         }
+        board.mHeight = lines.length;
+        board.mWidth = lines[0].length();
         board.mWidgetsRects = getRects(board.mWidget);
         board.mWidgetsRects.forEach(widgetRect -> {
             if (widgetRect.mType == CellType.MAIN_WIDGET) {
@@ -380,6 +411,11 @@
 
     public String toString(int maxX, int maxY) {
         StringBuilder s = new StringBuilder();
+        s.append("board: ");
+        s.append(maxX);
+        s.append("x");
+        s.append(maxY);
+        s.append("\n");
         maxX = Math.min(maxX, mWidget.length);
         maxY = Math.min(maxY, mWidget[0].length);
         for (int y = 0; y < maxY; y++) {
@@ -391,6 +427,11 @@
         return s.toString();
     }
 
+    @Override
+    public String toString() {
+        return toString(mWidth, mHeight);
+    }
+
     public static List<CellLayoutBoard> boardListFromString(String boardsStr) {
         String[] lines = boardsStr.split("\n");
         ArrayList<String> individualBoards = new ArrayList<>();
@@ -410,4 +451,12 @@
         }
         return boards;
     }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
index e3d07a9..0d2f252 100644
--- a/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
+++ b/tests/src/com/android/launcher3/celllayout/CellLayoutTestUtils.java
@@ -24,12 +24,12 @@
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
 import java.util.ArrayList;
+import java.util.List;
 
 public class CellLayoutTestUtils {
 
     public static ArrayList<CellLayoutBoard> workspaceToBoards(Launcher launcher) {
         ArrayList<CellLayoutBoard> boards = new ArrayList<>();
-        int widgetCount = 0;
         for (CellLayout cellLayout : launcher.getWorkspace().mWorkspaceScreens) {
 
             int count = cellLayout.getShortcutsAndWidgets().getChildCount();
@@ -52,11 +52,29 @@
                 } else {
                     // is widget
                     board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
-                            params.cellVSpan, (char) ('a' + widgetCount));
-                    widgetCount++;
+                            params.cellVSpan);
                 }
             }
         }
         return boards;
     }
+
+    public static CellLayoutBoard viewsToBoard(List<View> views, int width, int height) {
+        CellLayoutBoard board = new CellLayoutBoard();
+        board.mWidth = width;
+        board.mHeight = height;
+
+        for (View callView : views) {
+            CellLayoutLayoutParams params = (CellLayoutLayoutParams) callView.getLayoutParams();
+            // is icon
+            if (callView instanceof DoubleShadowBubbleTextView) {
+                board.addIcon(params.getCellX(), params.getCellY());
+            } else {
+                // is widget
+                board.addWidget(params.getCellX(), params.getCellY(), params.cellHSpan,
+                        params.cellVSpan);
+            }
+        }
+        return board;
+    }
 }
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
new file mode 100644
index 0000000..e3de500
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.celllayout;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.util.ActivityContextWrapper;
+import com.android.launcher3.views.DoubleShadowBubbleTextView;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ReorderAlgorithmUnitTest {
+    private Context mApplicationContext;
+
+    private int mPrevNumColumns, mPrevNumRows;
+
+    @Test
+    public void testAllCases() throws IOException {
+        List<ReorderAlgorithmUnitTestCase> testCases = getTestCases(
+                "ReorderAlgorithmUnitTest/reorder_algorithm_test_cases");
+        mApplicationContext = new ActivityContextWrapper(getApplicationContext());
+        List<Integer> failingCases = new ArrayList<>();
+        for (int i = 0; i < testCases.size(); i++) {
+            try {
+                evaluateTestCase(testCases.get(i));
+            } catch (AssertionError e) {
+                e.printStackTrace();
+                failingCases.add(i);
+            }
+        }
+        assertEquals("Some test cases failed " + Arrays.toString(failingCases.toArray()), 0,
+                failingCases.size());
+    }
+
+    private void addViewInCellLayout(CellLayout cellLayout, int cellX, int cellY, int spanX,
+            int spanY, boolean isWidget) {
+        View cell = isWidget ? new View(mApplicationContext) : new DoubleShadowBubbleTextView(
+                mApplicationContext);
+        cell.setLayoutParams(new CellLayoutLayoutParams(cellX, cellY, spanX, spanY));
+        cellLayout.addViewToCellLayout(cell, -1, cell.getId(),
+                (CellLayoutLayoutParams) cell.getLayoutParams(), true);
+    }
+
+    public CellLayout createCellLayout(int width, int height) {
+        Context c = mApplicationContext;
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
+        // modify the device profile.
+        dp.inv.numColumns = width;
+        dp.inv.numRows = height;
+
+        CellLayout cl = new CellLayout(getWrappedContext(c, dp));
+        // I put a very large number for width and height so that all the items can fit, it doesn't
+        // need to be exact, just bigger than the sum of cell border
+        cl.measure(View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY));
+
+        cl.measure(View.MeasureSpec.makeMeasureSpec(cl.getDesiredWidth(), View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(cl.getDesiredHeight(), View.MeasureSpec.EXACTLY));
+        return cl;
+    }
+
+    private Context getWrappedContext(Context context, DeviceProfile dp) {
+        return new ActivityContextWrapper(context) {
+            public DeviceProfile getDeviceProfile() {
+                return dp;
+            }
+        };
+    }
+
+    public CellLayout.ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
+            int spanY, int minSpanX, int minSpanY) {
+        CellLayout cl = createCellLayout(board.getWidth(), board.getHeight());
+
+        // The views have to be sorted or the result can vary
+        board.getIcons()
+                .stream()
+                .map(CellLayoutBoard.IconPoint::getCoord)
+                .sorted(Comparator.comparing(p -> ((Point) p).x).thenComparing(p -> ((Point) p).y))
+                .forEach(p -> addViewInCellLayout(cl, p.x, p.y, 1, 1, false));
+        board.getWidgets().stream()
+                .sorted(Comparator.comparing(CellLayoutBoard.WidgetRect::getCellX)
+                        .thenComparing(CellLayoutBoard.WidgetRect::getCellY))
+                .forEach(widget -> addViewInCellLayout(cl, widget.getCellX(), widget.getCellY(),
+                        widget.getSpanX(), widget.getSpanY(), true));
+
+        int[] testCaseXYinPixels = new int[2];
+        cl.regionToCenterPoint(x, y, spanX, spanY, testCaseXYinPixels);
+        CellLayout.ItemConfiguration solution = cl.createReorderAlgorithm().calculateReorder(
+                testCaseXYinPixels[0], testCaseXYinPixels[1], minSpanX, minSpanY, spanX, spanY,
+                null);
+        if (solution == null) {
+            solution = new CellLayout.ItemConfiguration();
+            solution.isSolution = false;
+        }
+        return solution;
+    }
+
+    public CellLayoutBoard boardFromSolution(CellLayout.ItemConfiguration solution, int width,
+            int height) {
+        // Update the views with solution value
+        solution.map.forEach((key, val) -> key.setLayoutParams(
+                new CellLayoutLayoutParams(val.cellX, val.cellY, val.spanX, val.spanY)));
+        CellLayoutBoard board = CellLayoutTestUtils.viewsToBoard(
+                new ArrayList<>(solution.map.keySet()), width, height);
+        board.addWidget(solution.cellX, solution.cellY, solution.spanX, solution.spanY,
+                'z');
+        return board;
+    }
+
+    public void evaluateTestCase(ReorderAlgorithmUnitTestCase testCase) {
+        CellLayout.ItemConfiguration solution = solve(testCase.startBoard, testCase.x,
+                testCase.y, testCase.spanX, testCase.spanY, testCase.minSpanX,
+                testCase.minSpanY);
+        assertEquals("should be a valid solution", solution.isSolution,
+                testCase.isValidSolution);
+        if (testCase.isValidSolution) {
+            CellLayoutBoard finishBoard = boardFromSolution(solution,
+                    testCase.startBoard.getWidth(), testCase.startBoard.getHeight());
+            assertTrue("End result and test case result board doesn't match ",
+                    finishBoard.compareTo(testCase.endBoard) == 0);
+        }
+    }
+
+    @Before
+    public void storePreviousValues() {
+        Context c = new ActivityContextWrapper(getApplicationContext());
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
+        mPrevNumColumns = dp.inv.numColumns;
+        mPrevNumRows = dp.inv.numColumns;
+    }
+
+    @After
+    public void restorePreviousValues() {
+        Context c = new ActivityContextWrapper(getApplicationContext());
+        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
+        dp.inv.numColumns = mPrevNumColumns;
+        dp.inv.numRows = mPrevNumRows;
+    }
+
+    @SuppressWarnings("UnusedMethod")
+    /**
+     * Utility function used to generate all the test cases
+     */
+    private ReorderAlgorithmUnitTestCase generateRandomTestCase() {
+        ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
+
+        int width = getRandom(3, 8);
+        int height = getRandom(3, 8);
+
+        int targetWidth = getRandom(1, width - 2);
+        int targetHeight = getRandom(1, height - 2);
+
+        int minTargetWidth = getRandom(1, targetWidth);
+        int minTargetHeight = getRandom(1, targetHeight);
+
+        int x = getRandom(0, width - targetWidth);
+        int y = getRandom(0, height - targetHeight);
+
+        CellLayoutBoard board = generateBoard(new CellLayoutBoard(width, height),
+                new Rect(0, 0, width, height), targetWidth * targetHeight);
+
+        CellLayout.ItemConfiguration solution = solve(board, x, y, targetWidth, targetHeight,
+                minTargetWidth, minTargetHeight);
+
+        CellLayoutBoard finishBoard = solution.isSolution ? boardFromSolution(solution,
+                board.getWidth(), board.getHeight()) : new CellLayoutBoard(board.getWidth(),
+                board.getHeight());
+
+
+        testCase.startBoard = board;
+        testCase.endBoard = finishBoard;
+        testCase.isValidSolution = solution.isSolution;
+        testCase.x = x;
+        testCase.y = y;
+        testCase.spanX = targetWidth;
+        testCase.spanY = targetHeight;
+        testCase.minSpanX = minTargetWidth;
+        testCase.minSpanY = minTargetHeight;
+        testCase.type = solution.area() == 1 ? "icon" : "widget";
+
+        return testCase;
+    }
+
+    private int getRandom(int start, int end) {
+        int random = end == 0 ? 0 : new Random().nextInt(end);
+        return start + random;
+    }
+
+    private CellLayoutBoard generateBoard(CellLayoutBoard board, Rect area,
+            int emptySpaces) {
+        if (area.height() * area.width() <= 0) return board;
+
+        int width = getRandom(1, area.width() - 1);
+        int height = getRandom(1, area.height() - 1);
+
+        int x = area.left + getRandom(0, area.width() - width);
+        int y = area.top + getRandom(0, area.height() - height);
+
+        if (emptySpaces > 0) {
+            emptySpaces -= width * height;
+        } else if (width * height > 1) {
+            board.addWidget(x, y, width, height);
+        } else {
+            board.addIcon(x, y);
+        }
+
+        generateBoard(board,
+                new Rect(area.left, area.top, area.right, y), emptySpaces);
+        generateBoard(board,
+                new Rect(area.left, y, x, area.bottom), emptySpaces);
+        generateBoard(board,
+                new Rect(x, y + height, area.right, area.bottom), emptySpaces);
+        generateBoard(board,
+                new Rect(x + width, y, area.right, y + height), emptySpaces);
+
+        return board;
+    }
+
+    private static List<ReorderAlgorithmUnitTestCase> getTestCases(String testPath)
+            throws IOException {
+        List<ReorderAlgorithmUnitTestCase> cases = new ArrayList<>();
+        Iterator<CellLayoutTestCaseReader.TestSection> iterableSection =
+                CellLayoutTestCaseReader.readFromFile(testPath).parse().iterator();
+        while (iterableSection.hasNext()) {
+            cases.add(ReorderAlgorithmUnitTestCase.readNextCase(iterableSection));
+        }
+        return cases;
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java
new file mode 100644
index 0000000..4274130
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTestCase.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.celllayout;
+
+import java.util.Iterator;
+
+/**
+ * Represents a test case for {@code ReorderAlgorithmUnitTest}. The test cases are generated from
+ * text, an example of a test is the following:
+ *
+ * board: 10x8
+ * aaaaaaaaai
+ * bbbbbcciii
+ * ---------f
+ * ---------f
+ * ---------f
+ * ---------i
+ * iiddddddii
+ * iieeiiiiii
+ * arguments: 2 5 7 1 3 1 widget valid
+ * board: 10x8
+ * bbbbbbbbbi
+ * eeeeecciii
+ * ---------a
+ * ---------a
+ * ---------a
+ * --zzzzzzzi
+ * iiddddddii
+ * iiffiiiiii
+ *
+ *
+ * This represents a Workspace boards and a dragged widget that wants to be dropped on the
+ * workspace. The endBoard represents the result from such drag
+ * The first board is the startBoard, the arguments are as follow: cellX, cellY, widget spanX,
+ * widget spanY, minimum spanX, minimum spanX, type of object being drag (icon, widget, folder ),
+ * if the resulting board is a valid solution or not reorder was found.
+ *
+ * For more information on how to read the board please go to the text file
+ * reorder_algorithm_test_cases
+ */
+public class ReorderAlgorithmUnitTestCase {
+
+    CellLayoutBoard startBoard;
+
+    int x, y, spanX, spanY, minSpanX, minSpanY;
+    String type;
+    boolean isValidSolution;
+    CellLayoutBoard endBoard;
+
+    public static ReorderAlgorithmUnitTestCase readNextCase(
+            Iterator<CellLayoutTestCaseReader.TestSection> sections) {
+        ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
+        CellLayoutTestCaseReader.Board startBoard =
+                (CellLayoutTestCaseReader.Board) sections.next();
+        testCase.startBoard = CellLayoutBoard.boardFromString(startBoard.board);
+        CellLayoutTestCaseReader.Arguments arguments =
+                (CellLayoutTestCaseReader.Arguments) sections.next();
+        testCase.x = Integer.parseInt(arguments.arguments[0]);
+        testCase.y = Integer.parseInt(arguments.arguments[1]);
+        testCase.spanX = Integer.parseInt(arguments.arguments[2]);
+        testCase.spanY = Integer.parseInt(arguments.arguments[3]);
+        testCase.minSpanX = Integer.parseInt(arguments.arguments[4]);
+        testCase.minSpanY = Integer.parseInt(arguments.arguments[5]);
+        testCase.type = arguments.arguments[6];
+        testCase.isValidSolution = arguments.arguments[7].compareTo("valid") == 0;
+
+        CellLayoutTestCaseReader.Board endBoard = (CellLayoutTestCaseReader.Board) sections.next();
+        testCase.endBoard = CellLayoutBoard.boardFromString(endBoard.board);
+        return testCase;
+    }
+
+    public CellLayoutBoard getStartBoard() {
+        return startBoard;
+    }
+
+    public int getX() {
+        return x;
+    }
+
+    public void setX(int x) {
+        this.x = x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public void setY(int y) {
+        this.y = y;
+    }
+
+    public int getSpanX() {
+        return spanX;
+    }
+
+    public void setSpanX(int spanX) {
+        this.spanX = spanX;
+    }
+
+    public int getSpanY() {
+        return spanY;
+    }
+
+    public void setSpanY(int spanY) {
+        this.spanY = spanY;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public boolean isValidSolution() {
+        return isValidSolution;
+    }
+
+    public void setValidSolution(boolean validSolution) {
+        isValidSolution = validSolution;
+    }
+
+    public CellLayoutBoard getEndBoard() {
+        return endBoard;
+    }
+
+    public void setEndBoard(CellLayoutBoard endBoard) {
+        this.endBoard = endBoard;
+    }
+
+    @Override
+    public String toString() {
+        String valid = isValidSolution ? "valid" : "invalid";
+        return startBoard + "arguments: " + x + " " + y + " " + spanX + " " + spanY + " " + minSpanX
+                + " " + minSpanY + " " + type + " " + valid + "\n" + endBoard;
+    }
+}
diff --git a/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt b/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt
new file mode 100644
index 0000000..fffa6d7
--- /dev/null
+++ b/tests/src/com/android/launcher3/logging/StartupLatencyLoggerTest.kt
@@ -0,0 +1,368 @@
+package com.android.launcher3.logging
+
+import androidx.core.util.isEmpty
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [StartupLatencyLogger]. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class StartupLatencyLoggerTest {
+
+    private val underTest: StartupLatencyLogger =
+        StartupLatencyLogger(StatsLogManager.StatsLatencyLogger.LatencyType.COLD)
+
+    @Before
+    fun setup() {
+        underTest.setIsInTest()
+    }
+
+    @Test
+    @UiThreadTest
+    fun logTotalDurationStart() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        val startTime =
+            underTest.startTimeByEvent.get(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.id
+            )
+        assertThat(startTime).isEqualTo(100)
+        assertThat(underTest.endTimeByEvent.isEmpty()).isTrue()
+    }
+
+    @Test
+    @UiThreadTest
+    fun logTotalDurationEnd() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        underTest.logEnd(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        val endTime =
+            underTest.endTimeByEvent.get(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.id
+            )
+        assertThat(endTime).isEqualTo(100)
+    }
+
+    @Test
+    @UiThreadTest
+    fun logStartOfOtherEvents_withoutLogStartOfTotalDuration_noOp() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE,
+                100
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION,
+                101
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent
+                    .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+                102
+            )
+
+        assertThat(underTest.startTimeByEvent.isEmpty()).isTrue()
+    }
+
+    @Test
+    @UiThreadTest
+    fun logStartOfOtherEvents_afterLogStartOfTotalDuration_logged() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE,
+                100
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION,
+                101
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent
+                    .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+                102
+            )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(4)
+    }
+
+    @Test
+    @UiThreadTest
+    fun logDuplicatedStartEvent_2ndEvent_notLogged() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            101
+        )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(1)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.id
+                )
+            )
+            .isEqualTo(100)
+    }
+
+    @Test
+    @UiThreadTest
+    fun loadStartOfWorkspace_thenEndWithAsync_logAsyncStart() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logWorkspaceLoadStartTime(111)
+
+        underTest.logEnd(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+            120
+        )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(2)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC
+                        .id
+                )
+            )
+            .isEqualTo(111)
+    }
+
+    @Test
+    @UiThreadTest
+    fun loadStartOfWorkspace_thenEndWithSync_logSyncStart() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logWorkspaceLoadStartTime(111)
+
+        underTest.logEnd(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC,
+            120
+        )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(2)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        .id
+                )
+            )
+            .isEqualTo(111)
+    }
+
+    @Test
+    @UiThreadTest
+    fun loadStartOfWorkspaceLoadSync_thenAsync_asyncNotLogged() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC,
+                110
+            )
+
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+            111
+        )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(2)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        .id
+                )
+            )
+            .isEqualTo(110)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC
+                        .id
+                )
+            )
+            .isEqualTo(0)
+    }
+
+    @Test
+    @UiThreadTest
+    fun loadStartOfWorkspaceLoadAsync_thenSync_syncNotLogged() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+            111
+        )
+
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC,
+            112
+        )
+
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(2)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC
+                        .id
+                )
+            )
+            .isEqualTo(111)
+        assertThat(
+                underTest.startTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        .id
+                )
+            )
+            .isEqualTo(0)
+    }
+
+    @Test
+    @UiThreadTest
+    fun logEndOfEvent_withoutStartEvent_notLogged() {
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        underTest.logEnd(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC,
+            120
+        )
+
+        assertThat(underTest.endTimeByEvent.size()).isEqualTo(0)
+        assertThat(
+                underTest.endTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        .id
+                )
+            )
+            .isEqualTo(0)
+    }
+
+    @Test
+    @UiThreadTest
+    fun logEndOfEvent_afterEndOfTotalDuration_notLogged() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                120
+            )
+
+        underTest.logEnd(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC,
+            121
+        )
+
+        assertThat(underTest.endTimeByEvent.size()).isEqualTo(1)
+        assertThat(
+                underTest.endTimeByEvent.get(
+                    StatsLogManager.LauncherLatencyEvent
+                        .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_SYNC
+                        .id
+                )
+            )
+            .isEqualTo(0)
+    }
+
+    @Test
+    @UiThreadTest
+    fun logCardinality_setCardinality() {
+        underTest.logCardinality(-1)
+        underTest.logStart(
+            StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+            100
+        )
+
+        underTest.logCardinality(235)
+
+        assertThat(underTest.cardinality).isEqualTo(235)
+    }
+
+    @Test
+    @UiThreadTest
+    fun reset_clearState() {
+        underTest
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                100
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE,
+                100
+            )
+            .logStart(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION,
+                110
+            )
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION,
+                115
+            )
+            .logWorkspaceLoadStartTime(120)
+            .logCardinality(235)
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE,
+                100
+            )
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent
+                    .LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC,
+                140
+            )
+            .logEnd(
+                StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION,
+                160
+            )
+        assertThat(underTest.startTimeByEvent.size()).isEqualTo(4)
+        assertThat(underTest.endTimeByEvent.size()).isEqualTo(4)
+        assertThat(underTest.cardinality).isEqualTo(235)
+
+        underTest.reset()
+
+        assertThat(underTest.startTimeByEvent.isEmpty()).isTrue()
+        assertThat(underTest.endTimeByEvent.isEmpty()).isTrue()
+        assertThat(underTest.cardinality).isEqualTo(StartupLatencyLogger.UNSET_INT)
+        assertThat(underTest.workspaceLoadStartTime).isEqualTo(StartupLatencyLogger.UNSET_LONG)
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index 0a1a9ba..cea95e5 100644
--- a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -39,6 +39,7 @@
 
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
+import com.android.launcher3.pm.UserCache;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -222,7 +223,9 @@
     private class MyDatabaseHelper extends DatabaseHelper {
 
         MyDatabaseHelper() {
-            super(mContext, DB_FILE, false);
+            super(mContext, DB_FILE,
+                    UserCache.INSTANCE.get(mContext)::getSerialNumberForUser,
+                    () -> { });
         }
 
         @Override
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index f24f0da..63dbaa7 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.model
 
+import android.content.ContentValues
 import android.content.Context
 import android.content.Intent
 import android.database.Cursor
@@ -23,6 +24,7 @@
 import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.android.launcher3.InvariantDeviceProfile
 import com.android.launcher3.LauncherPrefs
 import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
@@ -31,10 +33,7 @@
 import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.provider.LauncherDbUtils
-import com.android.launcher3.util.LauncherModelHelper
-import com.android.launcher3.util.LauncherModelHelper.*
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -43,11 +42,12 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class GridSizeMigrationUtilTest {
-    private lateinit var modelHelper: LauncherModelHelper
+
     private lateinit var context: Context
-    private lateinit var db: SQLiteDatabase
     private lateinit var validPackages: Set<String>
     private lateinit var idp: InvariantDeviceProfile
+    private lateinit var dbHelper: DatabaseHelper
+    private lateinit var db: SQLiteDatabase
     private val testPackage1 = "com.android.launcher3.validpackage1"
     private val testPackage2 = "com.android.launcher3.validpackage2"
     private val testPackage3 = "com.android.launcher3.validpackage3"
@@ -61,13 +61,17 @@
 
     @Before
     fun setUp() {
-        modelHelper = LauncherModelHelper()
-        context = modelHelper.sandboxContext
-        db = modelHelper.provider.db
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+        dbHelper =
+            DatabaseHelper(
+                context,
+                null,
+                UserCache.INSTANCE.get(context)::getSerialNumberForUser
+            ) {}
+        db = dbHelper.writableDatabase
 
         validPackages =
             setOf(
-                TEST_PACKAGE,
                 testPackage1,
                 testPackage2,
                 testPackage3,
@@ -86,11 +90,6 @@
         addTableToDb(db, userSerial, false, TMP_TABLE)
     }
 
-    @After
-    fun tearDown() {
-        modelHelper.destroy()
-    }
-
     /**
      * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is not
      * needed anymore
@@ -99,26 +98,26 @@
     @Throws(Exception::class)
     fun testMigration() {
         // Src Hotseat icons
-        modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
         // Src grid icons
         // _ _ _ _ _
         // _ _ _ _ 5
         // _ _ 6 _ 7
         // _ _ 8 _ 9
         // _ _ _ _ _
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage5, 5, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage6, 6, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage7, 7, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage8, 8, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 3, testPackage9, 9, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 1, testPackage5, 5, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage6, 6, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 2, testPackage7, 7, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 3, testPackage8, 8, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 3, testPackage9, 9, TMP_TABLE)
 
         // Dest hotseat icons
-        modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2)
+        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2)
         // Dest grid icons
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage10)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage10)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -126,8 +125,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -138,12 +136,13 @@
 
         // Check hotseat items
         var c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -168,12 +167,13 @@
 
         // Check workspace items
         c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -209,30 +209,30 @@
     fun testMigrationBackAndForth() {
         // Hotseat items in grid A
         // 1 2 _ 3 4
-        modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
         // Workspace items in grid A
         // _ _ _ _ _
         // _ _ _ _ 5
         // _ _ 6 _ 7
         // _ _ 8 _ _
         // _ _ _ _ _
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage5, 5, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage6, 6, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage7, 7, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage8, 8, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 1, testPackage5, 5, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage6, 6, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 4, 2, testPackage7, 7, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 3, testPackage8, 8, TMP_TABLE)
 
         // Hotseat items in grid B
         // 2 _ _ _
-        modelHelper.addItem(SHORTCUT, 0, HOTSEAT, 0, 0, testPackage2)
+        addItem(ITEM_TYPE_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage2)
         // Workspace items in grid B
         // _ _ _ _
         // _ _ _ 10
         // _ _ _ _
         // _ _ _ _
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 3, testPackage10)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 3, testPackage10)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -241,8 +241,7 @@
         val readerGridB = DbReader(db, TABLE_NAME, context, validPackages)
         // migrate from A -> B
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
@@ -253,12 +252,13 @@
 
         // Check hotseat items in grid B
         var c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -272,12 +272,13 @@
 
         // Check workspace items in grid B
         c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -294,12 +295,11 @@
         assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))
 
         // add item in B
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 2, testPackage9)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9)
 
         // migrate from B -> A
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             readerGridB,
             readerGridA,
             5,
@@ -309,12 +309,13 @@
         )
         // Check hotseat items in grid A
         c =
-            context.contentResolver.query(
-                TMP_CONTENT_URI,
+            db.query(
+                TMP_TABLE,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -328,12 +329,13 @@
 
         // Check workspace items in grid A
         c =
-            context.contentResolver.query(
-                TMP_CONTENT_URI,
+            db.query(
+                TMP_TABLE,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -354,12 +356,11 @@
         assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
 
         // remove item from B
-        modelHelper.deleteItem(7, TMP_TABLE)
+        db.delete(TMP_TABLE, "$_ID=7", null)
 
         // migrate from A -> B
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
@@ -370,12 +371,13 @@
 
         // Check hotseat items in grid B
         c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -389,12 +391,13 @@
 
         // Check workspace items in grid B
         c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -443,10 +446,28 @@
     fun migrateToLargerHotseat() {
         val srcHotseatItems =
             intArrayOf(
-                modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
-                modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
-                modelHelper.addItem(APP_ICON, 2, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
-                modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
+                addItem(
+                    ITEM_TYPE_APPLICATION,
+                    0,
+                    CONTAINER_HOTSEAT,
+                    0,
+                    0,
+                    testPackage1,
+                    1,
+                    TMP_TABLE
+                ),
+                addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE),
+                addItem(
+                    ITEM_TYPE_APPLICATION,
+                    2,
+                    CONTAINER_HOTSEAT,
+                    0,
+                    0,
+                    testPackage3,
+                    3,
+                    TMP_TABLE
+                ),
+                addItem(ITEM_TYPE_SHORTCUT, 3, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
             )
         val numSrcDatabaseHotseatIcons = srcHotseatItems.size
         idp.numDatabaseHotseatIcons = 6
@@ -455,8 +476,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -467,12 +487,13 @@
 
         // Check hotseat items
         val c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -501,11 +522,11 @@
 
     @Test
     fun migrateFromLargerHotseat() {
-        modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 2, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI)
-        modelHelper.addItem(SHORTCUT, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 5, HOTSEAT, 0, 0, testPackage5, 5, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 3, CONTAINER_HOTSEAT, 0, 0, testPackage3, 3, TMP_TABLE)
+        addItem(ITEM_TYPE_SHORTCUT, 4, CONTAINER_HOTSEAT, 0, 0, testPackage4, 4, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 5, CONTAINER_HOTSEAT, 0, 0, testPackage5, 5, TMP_TABLE)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -513,8 +534,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -525,12 +545,13 @@
 
         // Check hotseat items
         val c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -568,11 +589,11 @@
         enableNewMigrationLogic("4,4")
 
         // Setup src grid
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage1, 5, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage2, 6, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 3, 1, testPackage3, 7, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 3, 2, testPackage4, 8, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 2, DESKTOP, 3, 3, testPackage5, 9, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage1, 5, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 3, testPackage2, 6, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 1, testPackage3, 7, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 2, testPackage4, 8, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_DESKTOP, 3, 3, testPackage5, 9, TMP_TABLE)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 6
@@ -581,8 +602,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -593,12 +613,13 @@
 
         // Get workspace items
         val c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -630,11 +651,11 @@
         enableNewMigrationLogic("2,2")
 
         // Setup src grid
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 1, testPackage1, 5, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 1, testPackage2, 6, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 0, 0, testPackage3, 7, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 1, 0, testPackage4, 8, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 2, DESKTOP, 0, 0, testPackage5, 9, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage1, 5, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 1, testPackage2, 6, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 0, 0, testPackage3, 7, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 1, 0, testPackage4, 8, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_DESKTOP, 0, 0, testPackage5, 9, TMP_TABLE)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 5
@@ -642,8 +663,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -654,12 +674,13 @@
 
         // Get workspace items
         val c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -691,11 +712,11 @@
         enableNewMigrationLogic("5,5")
 
         // Setup src grid
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 1, testPackage1, 5, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 1, testPackage2, 6, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 0, 0, testPackage3, 7, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 1, DESKTOP, 1, 0, testPackage4, 8, TMP_CONTENT_URI)
-        modelHelper.addItem(APP_ICON, 2, DESKTOP, 0, 0, testPackage5, 9, TMP_CONTENT_URI)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 1, testPackage1, 5, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 1, testPackage2, 6, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 0, 0, testPackage3, 7, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 1, 0, testPackage4, 8, TMP_TABLE)
+        addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_DESKTOP, 0, 0, testPackage5, 9, TMP_TABLE)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -703,8 +724,7 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            context,
-            db,
+            dbHelper,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -715,12 +735,13 @@
 
         // Get workspace items
         val c =
-            context.contentResolver.query(
-                CONTENT_URI,
+            db.query(
+                TABLE_NAME,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
+                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -747,4 +768,48 @@
     private fun enableNewMigrationLogic(srcGridSize: String) {
         LauncherPrefs.get(context).putSync(WORKSPACE_SIZE.to(srcGridSize))
     }
+
+    private fun addItem(
+        type: Int,
+        screen: Int,
+        container: Int,
+        x: Int,
+        y: Int,
+        packageName: String?
+    ): Int {
+        return addItem(
+            type,
+            screen,
+            container,
+            x,
+            y,
+            packageName,
+            dbHelper.generateNewItemId(),
+            TABLE_NAME
+        )
+    }
+
+    private fun addItem(
+        type: Int,
+        screen: Int,
+        container: Int,
+        x: Int,
+        y: Int,
+        packageName: String?,
+        id: Int,
+        tableName: String
+    ): Int {
+        val values = ContentValues()
+        values.put(_ID, id)
+        values.put(CONTAINER, container)
+        values.put(SCREEN, screen)
+        values.put(CELLX, x)
+        values.put(CELLY, y)
+        values.put(SPANX, 1)
+        values.put(SPANY, 1)
+        values.put(ITEM_TYPE, type)
+        values.put(INTENT, Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0))
+        db.insert(tableName, null, values)
+        return id
+    }
 }
diff --git a/tests/src/com/android/launcher3/model/LoaderCursorTest.java b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
index d192be4..78812c0 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -59,7 +59,6 @@
 
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.util.Executors;
@@ -102,7 +101,7 @@
         });
 
         UserManagerState ums = new UserManagerState();
-        mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
+        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums);
         ums.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index 42c9f11..b9da16a 100644
--- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -180,7 +180,8 @@
         }
 
         @Override
-        public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+        public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+                int workspaceItemCount, boolean isBindSync) {
             mPageBoundSync = boundPages;
             mPendingTasks = pendingTasks;
         }
diff --git a/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java b/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
index 2b6f9ff..54b8489 100644
--- a/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
+++ b/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.model.DbDowngradeHelper;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.settings.SettingsActivity;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.IOUtils;
@@ -165,12 +166,11 @@
     private class MyDatabaseHelper extends DatabaseHelper {
 
         MyDatabaseHelper() {
-            super(mContext, null, false);
+            super(mContext, null, UserCache.INSTANCE.get(mContext)::getSerialNumberForUser,
+                    () -> { });
         }
 
         @Override
         protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
-
-        protected void onEmptyDbCreated() { }
     }
 }
diff --git a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
index 67de1f5..1b1b294 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.DatabaseHelper;
+import com.android.launcher3.model.ModelDbController;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,13 +64,13 @@
 
     @Test
     public void testGetProfileId() throws Exception {
-        SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
+        SQLiteDatabase db = new MyModelDbController(23).getDb();
         assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
     }
 
     @Test
     public void testMigrateProfileId() throws Exception {
-        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+        SQLiteDatabase db = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
@@ -89,7 +90,7 @@
 
     @Test
     public void testChangeDefaultColumn() throws Exception {
-        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+        SQLiteDatabase db = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
@@ -120,20 +121,20 @@
         long workProfileId = myProfileId + 2;
         long workProfileId_old = myProfileId + 3;
 
-        MyDatabaseHelper helper = new MyDatabaseHelper(myProfileId);
-        SQLiteDatabase db = helper.getWritableDatabase();
+        MyModelDbController controller = new MyModelDbController(myProfileId);
+        SQLiteDatabase db = controller.getDb();
         BackupManager bm = spy(new BackupManager(context));
         doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
         doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
-        helper.users.put(workProfileId, mWorkUser);
+        controller.users.put(workProfileId, mWorkUser);
 
-        addIconsBulk(helper, 10, 1, myProfileId_old);
-        addIconsBulk(helper, 6, 2, workProfileId_old);
+        addIconsBulk(controller, 10, 1, myProfileId_old);
+        addIconsBulk(controller, 6, 2, workProfileId_old);
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
         RestoreDbTask task = new RestoreDbTask();
-        task.sanitizeDB(context, helper, helper.getWritableDatabase(), bm);
+        task.sanitizeDB(context, controller, controller.getDb(), bm);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -151,20 +152,20 @@
         long myProfileId_old = myProfileId + 1;
         long workProfileId_old = myProfileId + 3;
 
-        MyDatabaseHelper helper = new MyDatabaseHelper(myProfileId);
-        SQLiteDatabase db = helper.getWritableDatabase();
+        MyModelDbController controller = new MyModelDbController(myProfileId);
+        SQLiteDatabase db = controller.getDb();
         BackupManager bm = spy(new BackupManager(context));
         doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
         // Work profile is not migrated
         doReturn(null).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
 
-        addIconsBulk(helper, 10, 1, myProfileId_old);
-        addIconsBulk(helper, 6, 2, workProfileId_old);
+        addIconsBulk(controller, 10, 1, myProfileId_old);
+        addIconsBulk(controller, 6, 2, workProfileId_old);
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
         RestoreDbTask task = new RestoreDbTask();
-        task.sanitizeDB(context, helper, helper.getWritableDatabase(), bm);
+        task.sanitizeDB(context, controller, controller.getDb(), bm);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -173,12 +174,13 @@
         assertEquals(10, getCount(db, "select * from favorites"));
     }
 
-    private void addIconsBulk(DatabaseHelper helper, int count, int screen, long profileId) {
+    private void addIconsBulk(MyModelDbController controller,
+            int count, int screen, long profileId) {
         int columns = LauncherAppState.getIDP(getInstrumentation().getTargetContext()).numColumns;
         String packageName = getInstrumentation().getContext().getPackageName();
         for (int i = 0; i < count; i++) {
             ContentValues values = new ContentValues();
-            values.put(LauncherSettings.Favorites._ID, helper.generateNewItemId());
+            values.put(LauncherSettings.Favorites._ID, controller.generateNewItemId());
             values.put(LauncherSettings.Favorites.CONTAINER, CONTAINER_DESKTOP);
             values.put(LauncherSettings.Favorites.SCREEN, screen);
             values.put(LauncherSettings.Favorites.CELLX, i % columns);
@@ -189,11 +191,11 @@
             values.put(LauncherSettings.Favorites.ITEM_TYPE, ITEM_TYPE_APPLICATION);
             values.put(LauncherSettings.Favorites.INTENT,
                     new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
-            helper.getWritableDatabase().insert(TABLE_NAME, null, values);
+
+            controller.insert(TABLE_NAME, values);
         }
     }
 
-
     @Test
     public void testRemoveScreenIdGaps_firstScreenEmpty() {
         runRemoveScreenIdGapsTest(
@@ -216,7 +218,7 @@
     }
 
     private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) {
-        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
+        SQLiteDatabase db = new MyModelDbController(42).getDb();
         // Add some mock data
         for (int i = 0; i < screenIds.length; i++) {
             ContentValues values = new ContentValues();
@@ -254,14 +256,15 @@
         }
     }
 
-    private class MyDatabaseHelper extends DatabaseHelper {
+    private class MyModelDbController extends ModelDbController {
 
-        public final LongSparseArray<UserHandle> users;
+        public final LongSparseArray<UserHandle> users = new LongSparseArray<>();
 
-        MyDatabaseHelper(long profileId) {
-            super(getInstrumentation().getTargetContext(), null, false);
-            users = new LongSparseArray<>();
+        MyModelDbController(long profileId) {
+            super(getInstrumentation().getTargetContext());
             users.put(profileId, myUserHandle());
+            mOpenHelper = new DatabaseHelper(getInstrumentation().getTargetContext(), null,
+                    this::getSerialNumberForUser, () -> { });
         }
 
         @Override
@@ -269,10 +272,5 @@
             int index = users.indexOfValue(user);
             return index >= 0 ? users.keyAt(index) : -1;
         }
-
-        @Override
-        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
-
-        protected void onEmptyDbCreated() { }
     }
 }
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
index bf31e39..6fca965 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -363,12 +363,6 @@
         sandboxContext.getContentResolver().insert(contentUri, values);
     }
 
-    public void deleteItem(int itemId, @NonNull final String tableName) {
-        final Uri uri = Uri.parse("content://"
-                + LauncherProvider.AUTHORITY + "/" + tableName + "/" + itemId);
-        sandboxContext.getContentResolver().delete(uri, null, null);
-    }
-
     /**
      * Sets up a mock provider to load the provided layout by default, next time the layout loads
      */
@@ -426,7 +420,7 @@
         }
 
         public SQLiteDatabase getDb() {
-            return getModelDbController().getDatabaseHelper().getWritableDatabase();
+            return getModelDbController().getDb();
         }
     }